diff --git a/.agents/skills/add-ai-integration/SKILL.md b/.agents/skills/add-ai-integration/SKILL.md index c3b823fdfa17..278160f4133c 100644 --- a/.agents/skills/add-ai-integration/SKILL.md +++ b/.agents/skills/add-ai-integration/SKILL.md @@ -10,7 +10,7 @@ argument-hint: Span ops and attributes are specified outside this repo. Never invent or hardcode either: -- [gen_ai attributes](https://getsentry.github.io/sentry-conventions/attributes/gen_ai/) and [gen_ai ops](https://getsentry.github.io/sentry-conventions/ops/#gen_ai) — normative; import from `@sentry/conventions/attributes` and `@sentry/conventions/op` +- [gen_ai attributes](https://conventions.sentry.dev/attributes/gen_ai/) and [gen_ai ops](https://conventions.sentry.dev/ops/#gen_ai) — normative; import from `@sentry/conventions/attributes` and `@sentry/conventions/op` - [RFC 0153](https://github.com/getsentry/rfcs/blob/main/text/0153-decoupling-sentrys-generative-ai-conventions-from-open-telemetry.md) — why Sentry's gen-AI conventions diverge from the [OTel gen-ai semconv](https://opentelemetry.io/docs/specs/semconv/gen-ai/) Derive the op with `getGenAiSpanOp()` from `ai/core/utils.ts` rather than picking one by hand. `ai/core/gen-ai-attributes.ts` is for gap-fillers only — keys with no `@sentry/conventions` equivalent — so check it last, not first. diff --git a/.agents/skills/port-span-names/SKILL.md b/.agents/skills/port-span-names/SKILL.md index f6f35d0c7bcb..930afa8bce7a 100644 --- a/.agents/skills/port-span-names/SKILL.md +++ b/.agents/skills/port-span-names/SKILL.md @@ -6,7 +6,7 @@ argument-hint: ' [constraints] # e.g. navigation, or "db.query --only # Port a span op to low-cardinality span names -Span names must be low cardinality, per the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/): a raw URL, id, or query must never end up in a span name. Where the SDK has no low-cardinality value for a span, it uses a **static fallback name** for that op instead. +Span names must be low cardinality, per the [Sentry span name conventions](https://conventions.sentry.dev/names/): a raw URL, id, or query must never end up in a span name. Where the SDK has no low-cardinality value for a span, it uses a **static fallback name** for that op instead. This only applies when span streaming is enabled. With `traceLifecycle: 'static'` every span name must stay byte-identical to before. @@ -37,13 +37,13 @@ These are non-negotiable. Every one of them was arrived at by rejecting the alte ## 1. Look up the convention -Read and find the op. Each op lists attribute templates in priority order, ending in a static fallback — that fallback is your name. Examples: `pageload` → `Pageload`, `navigation` → `Navigation`, database ops → `Database operation`. +Read and find the op. Each op lists attribute templates in priority order, ending in a static fallback — that fallback is your name. Examples: `pageload` → `Pageload`, `navigation` → `Navigation`, database ops → `Database operation`. Add it next to `PAGELOAD_SPAN_NAME_FALLBACK` in `packages/core/src/tracing/spans/spanNames.ts` and export it from `shared-exports.ts`. Every package imports it from `@sentry/core` directly — no re-export from `@sentry/browser` is needed. ### Then look up the description rules -The name is only half the contract. Relay re-derives a span's **description** from its attributes using the [span description rules](https://getsentry.github.io/sentry-conventions/descriptions/) — a separate, much shorter set of templates from the name rules. `function`, for example, has exactly one (`{{code.function.name}}`) and no static fallback, so a `function` span with no `code.function.name` gets no inferred description at all. +The name is only half the contract. Relay re-derives a span's **description** from its attributes using the [span description rules](https://conventions.sentry.dev/descriptions/) — a separate, much shorter set of templates from the name rules. `function`, for example, has exactly one (`{{code.function.name}}`) and no static fallback, so a `function` span with no `code.function.name` gets no inferred description at all. Renaming a span therefore drops information unless you check what the op's description template can still produce. For each site, in this order: diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 5f51f918c34b..052da6857343 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -22,7 +22,9 @@ runs: id: cache_dependencies with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ steps.compute_lockfile_hash.outputs.hash }} + key: + ${{ steps.compute_lockfile_hash.outputs.hash }}${{ runner.arch != 'X64' && format('-{0}', runner.arch) || '' + }} - name: Install dependencies if: steps.cache_dependencies.outputs.cache-hit != 'true' diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index ca6c8a2d9583..b642f77815d3 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -38,22 +38,95 @@ runs: # We always install all browsers, if uncached # `npx` runs outside the repo, so it can't pick up a `playwright` that a parallel `pnpm install` is still linking. - - name: Install Playwright dependencies (uncached) + - name: Install Playwright browsers (uncached) env: PLAYWRIGHT_VERSION: ${{ steps.playwright-version.outputs.version }} - run: npx -y "playwright@$PLAYWRIGHT_VERSION" install chromium webkit firefox --with-deps + run: npx -y "playwright@$PLAYWRIGHT_VERSION" install chromium webkit firefox if: steps.playwright-version.outputs.version != '' && steps.playwright-cache.outputs.cache-hit != 'true' shell: bash working-directory: ${{ runner.temp }} - - name: Install Playwright system dependencies only (cached) - env: - PLAYWRIGHT_BROWSERS: ${{ inputs.browsers || 'chromium webkit firefox' }} - PLAYWRIGHT_VERSION: ${{ steps.playwright-version.outputs.version }} - run: npx -y "playwright@$PLAYWRIGHT_VERSION" install-deps $PLAYWRIGHT_BROWSERS - if: steps.playwright-version.outputs.version != '' && steps.playwright-cache.outputs.cache-hit == 'true' + - name: Resolve headless WebKit system dependencies + id: webkit-dependencies + if: steps.playwright-version.outputs.version != '' && contains(inputs.browsers, 'webkit') + run: | + packages=( + gstreamer1.0-plugins-base + gstreamer1.0-plugins-good + libavif16 + libevent-2.1-7t64 + libflite1 + libgles2 + libgstreamer-gl1.0-0 + libgstreamer-plugins-bad1.0-0 + libgstreamer-plugins-base1.0-0 + libharfbuzz-icu0 + libhyphen0 + libmanette-0.2-0 + libopus0 + libwayland-server0 + ) + # Resolve now and install after restoring the cache; share the package list across both steps. + printf '%s\n' "${packages[@]}" > "$RUNNER_TEMP/webkit-apt-packages" + # runner.temp is private, so keep installers where apt's sandbox user can access them. + mkdir -p /var/tmp/webkit-apt-cache + + sudo apt-get update + # Ask apt which installers it needs, including dependencies, without downloading them. + # Do this before restoring cached files so the key describes the full download set. + plan=$(sudo apt-get \ + -o Dir::Cache::archives=/var/tmp/webkit-apt-cache \ + --print-uris --download-only -y --no-install-recommends \ + install "${packages[@]}") + # Hash only download records, in a stable order, so package updates produce a new key. + hash=$(printf '%s\n' "$plan" | sed -n "/^'/p" | LC_ALL=C sort | sha256sum | cut -d ' ' -f1) + + # Separate Ubuntu versions and architectures, while allowing older package sets as fallbacks. + . /etc/os-release + prefix="webkit-apt-$ID-$VERSION_ID-${{ runner.arch }}-v1-" + echo "prefix=$prefix" >> "$GITHUB_OUTPUT" + echo "key=$prefix$hash" >> "$GITHUB_OUTPUT" shell: bash - working-directory: ${{ runner.temp }} + + - name: Restore cached WebKit system packages + id: webkit-apt-cache + if: steps.webkit-dependencies.outputs.key != '' + uses: actions/cache/restore@v6.1.0 + with: + path: /var/tmp/webkit-apt-cache/*.deb + key: ${{ steps.webkit-dependencies.outputs.key }} + # If this exact package set isn't cached, reuse an older set; apt downloads only what's missing. + restore-keys: ${{ steps.webkit-dependencies.outputs.prefix }} + + - name: Install headless WebKit system dependencies + if: steps.webkit-dependencies.outputs.key != '' + run: | + mapfile -t packages < "$RUNNER_TEMP/webkit-apt-packages" + # Install from restored .deb files where possible, retaining new downloads for the cache save. + sudo apt-get \ + -o Dir::Cache::archives=/var/tmp/webkit-apt-cache \ + -o APT::Keep-Downloaded-Packages=true \ + install -y --no-install-recommends "${packages[@]}" + shell: bash + + - name: Remove obsolete cached WebKit packages + if: + steps.webkit-dependencies.outputs.key != '' && steps.webkit-apt-cache.outputs.cache-hit != 'true' && + github.event_name == 'push' && github.ref == 'refs/heads/develop' + # Fallback restores can carry obsolete installers into newer cache entries. + run: sudo apt-get -o Dir::Cache::archives=/var/tmp/webkit-apt-cache autoclean + shell: bash + + # Develop populates caches shared with PRs. Save after misses or fallback restores, + # which may have downloaded packages absent from the restored cache. + - name: Store cached WebKit system packages + if: + steps.webkit-dependencies.outputs.key != '' && steps.webkit-apt-cache.outputs.cache-hit != 'true' && + github.event_name == 'push' && github.ref == 'refs/heads/develop' + uses: actions/cache/save@v6.1.0 + with: + path: /var/tmp/webkit-apt-cache/*.deb + key: ${{ steps.webkit-dependencies.outputs.key }} # Only store cache on develop branch - name: Store cached playwright binaries diff --git a/.github/actions/prepare-e2e-test/action.yml b/.github/actions/prepare-e2e-test/action.yml index 6af9d8ce37a6..417877403d90 100644 --- a/.github/actions/prepare-e2e-test/action.yml +++ b/.github/actions/prepare-e2e-test/action.yml @@ -68,7 +68,7 @@ runs: github-token: ${{ inputs.github-token }} - name: Restore build layer artifacts - if: contains(inputs.test-application, 'aws-serverless') + if: inputs.test-application == 'aws-serverless-layer' uses: actions/download-artifact@v8 with: name: build-layer-output diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml index daa5943f9877..372def6a0a7e 100644 --- a/.github/actions/restore-cache/action.yml +++ b/.github/actions/restore-cache/action.yml @@ -17,7 +17,7 @@ runs: uses: actions/cache/restore@v6.1.0 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ inputs.dependency_cache_key }} + key: ${{ inputs.dependency_cache_key }}${{ runner.arch != 'X64' && format('-{0}', runner.arch) || '' }} - name: Restore build artifacts uses: actions/download-artifact@v7 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c408c806846..59211cdc3d6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: job_build: name: Build needs: job_get_metadata - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 if: | needs.job_get_metadata.outputs.changed_any_code == 'true' || @@ -128,7 +128,7 @@ jobs: path: | .nx/cache .nx/workspace-data - key: ${{ env.NX_CACHE_KEY }} + key: ${{ env.NX_CACHE_KEY }}-${{ runner.arch }} - name: Upload build artifacts uses: actions/upload-artifact@v7 @@ -142,9 +142,9 @@ jobs: - name: Determine which test applications should be run id: matrix run: - pnpm --silent run ci:build-matrix --shards=2 --base=${{ (github.event_name == 'pull_request' && - needs.job_get_metadata.outputs.changed_ci != 'true' && github.event.pull_request.base.sha) || '' }} >> - $GITHUB_OUTPUT + pnpm --silent run ci:build-matrix --shards=2 --separate-aws-layer --base=${{ (github.event_name == + 'pull_request' && needs.job_get_metadata.outputs.changed_ci != 'true' && github.event.pull_request.base.sha) + || '' }} >> $GITHUB_OUTPUT working-directory: dev-packages/e2e-tests - name: Determine which optional E2E test applications should be run @@ -189,15 +189,15 @@ jobs: '@sentry/aws-serverless') }} e2e-matrix-1: ${{ steps.matrix.outputs.matrix-1 }} e2e-matrix-2: ${{ steps.matrix.outputs.matrix-2 }} + e2e-matrix-aws-layer: ${{ steps.matrix.outputs.matrix-aws-layer }} e2e-matrix-optional: ${{ steps.matrix-optional.outputs.matrix }} job_build_layer: name: Build Lambda layer needs: [job_get_metadata, job_build] if: - needs.job_build.outputs.changed_aws_serverless == 'true' || contains(needs.job_build.outputs.e2e-matrix-1, - 'aws-serverless') || contains(needs.job_build.outputs.e2e-matrix-2, 'aws-serverless') || github.event_name != - 'pull_request' + needs.job_build.outputs.changed_aws_serverless == 'true' || contains(needs.job_build.outputs.e2e-matrix-aws-layer, + 'aws-serverless-layer') || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-24.04 steps: @@ -215,7 +215,7 @@ jobs: path: | .nx/cache .nx/workspace-data - key: ${{ env.NX_CACHE_KEY }} + key: ${{ env.NX_CACHE_KEY }}-${{ runner.arch }} - name: Restore caches uses: ./.github/actions/restore-cache @@ -238,7 +238,7 @@ jobs: name: Build bundles needs: [job_get_metadata, job_build] timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v7 @@ -254,7 +254,7 @@ jobs: path: | .nx/cache .nx/workspace-data - key: ${{ env.NX_CACHE_KEY }} + key: ${{ env.NX_CACHE_KEY }}-${{ runner.arch }} - name: Restore caches uses: ./.github/actions/restore-cache @@ -292,7 +292,7 @@ jobs: name: Size Check needs: [job_get_metadata, job_build, job_build_bundles] timeout-minutes: 15 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm if: github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_base_branch == 'true' || needs.job_get_metadata.outputs.is_release == 'true' @@ -327,7 +327,7 @@ jobs: # inter-package dependencies resolve cleanly. needs: [job_get_metadata, job_build] timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v7 @@ -509,7 +509,7 @@ jobs: needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_bun == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm strategy: fail-fast: false matrix: @@ -541,7 +541,7 @@ jobs: needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_deno == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v7 @@ -569,7 +569,7 @@ jobs: name: Node (${{ matrix.node }}) Unit Tests needs: [job_get_metadata, job_build] timeout-minutes: 10 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm # Node 27 is an alpha: its ABI and APIs can change, so its failures must not fail the required checks. continue-on-error: ${{ matrix.node == '^27.0.0-0' }} strategy: @@ -623,8 +623,9 @@ jobs: strategy: fail-fast: false matrix: + shard: [1, 2] + shards: [2] bundle: - - esm - bundle - bundle_min - bundle_logs_metrics @@ -640,10 +641,14 @@ jobs: project: - chromium include: - # Only check all projects for full bundle - # We also shard the tests as they take the longest - bundle: bundle_tracing_replay_feedback_logs_metrics_min project: 'webkit' + shard: 1 + shards: 2 + - bundle: bundle_tracing_replay_feedback_logs_metrics_min + project: 'webkit' + shard: 2 + shards: 2 - bundle: bundle_tracing_replay_feedback_logs_metrics_min project: 'firefox' - bundle: esm @@ -662,10 +667,6 @@ jobs: project: chromium shard: 4 shards: 4 - exclude: - # Do not run the un-sharded esm tests - - bundle: esm - project: 'chromium' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -773,18 +774,29 @@ jobs: retention-days: 7 job_node_integration_tests: - name: Node (${{ matrix.node }}) Integration Tests (${{ matrix.shard }}/3) + name: Node (${{ matrix.node }}) Integration Tests (${{ matrix.shard }}/${{ matrix.shards }}) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 20 # Node 27 is an alpha: its ABI and APIs can change, so its failures must not fail the required checks. continue-on-error: ${{ matrix.node == '^27.0.0-0' }} strategy: fail-fast: false matrix: - node: [20.19, 22, 24, 26, '^27.0.0-0'] - shard: [1, 2, 3] + node: [20.19, 22, 24, 26] + shard: [1, 2, 3, 4] + shards: [4] + include: + - node: '^27.0.0-0' + shard: 1 + shards: 3 + - node: '^27.0.0-0' + shard: 2 + shards: 3 + - node: '^27.0.0-0' + shard: 3 + shards: 3 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v7 @@ -819,11 +831,37 @@ jobs: - name: Run integration tests working-directory: dev-packages/node-integration-tests - run: pnpm run test --shard=${{ matrix.shard }}/3 + # Tedious runs in the dedicated SQL Server job because its database container requires x86. + run: pnpm run test --shard=${{ matrix.shard }}/${{ matrix.shards }} --exclude '**/tracing/tedious/**' + + job_sql_server_integration_tests: + name: SQL Server Integration Tests (Node 24) + needs: [job_get_metadata, job_build] + if: >- + needs.job_build.outputs.changed_node_integration == 'true' || needs.job_build.outputs.changed_bun_integration == + 'true' || needs.job_build.outputs.changed_deno_integration == 'true' || github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v7 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: '24' + - name: Restore caches + uses: ./.github/actions/restore-cache + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Run SQL Server integration tests + working-directory: dev-packages/node-integration-tests + run: pnpm run test suites/tracing/tedious/test.ts # Runs on the wrangler pinned in the package, so the required run stays on a fixed, older runtime. job_cloudflare_integration_tests: - name: Cloudflare Integration Tests (${{ matrix.shard }}/2) + name: Cloudflare Integration Tests (${{ matrix.shard }}/${{ matrix.shards }}) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_cloudflare_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-24.04 @@ -831,7 +869,8 @@ jobs: strategy: fail-fast: false matrix: - shard: [1, 2] + shard: [1, 2, 3, 4] + shards: [4] steps: &cloudflare_integration_tests_steps - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v7 @@ -858,7 +897,7 @@ jobs: - name: Run integration tests working-directory: dev-packages/cloudflare-integration-tests - run: pnpm run test --shard=${{ matrix.shard }}/2 + run: pnpm run test --shard=${{ matrix.shard }}/${{ matrix.shards }} # Shows breakage from new wrangler and workerd releases early. It is not in `job_required_jobs_passed`, so it cannot # block a PR, and like the optional E2E tests it does not run on release branches. @@ -868,12 +907,13 @@ jobs: if: needs.job_get_metadata.outputs.is_release != 'true' && (needs.job_build.outputs.changed_cloudflare_integration == 'true' || github.event_name != 'pull_request') - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 strategy: fail-fast: false matrix: shard: [1, 2] + shards: [2] wrangler: [latest] steps: *cloudflare_integration_tests_steps @@ -881,7 +921,7 @@ jobs: name: Bundler Plugin Integration Tests (Node ${{ matrix.node }}) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_bundler_plugin_integration == 'true' || github.event_name != 'pull_request' - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 # Node 27 is an alpha: its ABI and APIs can change, so its failures must not fail the required checks. continue-on-error: ${{ matrix.node == '^27.0.0-0' }} @@ -911,7 +951,7 @@ jobs: name: Bun (${{ matrix.bun }}) Integration Tests (${{ matrix.shard }}/2) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_bun_integration == 'true' || github.event_name != 'pull_request' - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 strategy: fail-fast: false @@ -945,7 +985,7 @@ jobs: name: Deno Integration Tests (${{ matrix.shard }}/2) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_deno_integration == 'true' || github.event_name != 'pull_request' - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 strategy: fail-fast: false @@ -980,7 +1020,8 @@ jobs: pnpm run test:unit - name: Run shared Node integration tests working-directory: dev-packages/deno-integration-tests - run: pnpm run test:node-suites --shard=${{ matrix.shard }}/2 + # Tedious runs in the dedicated SQL Server job because its database container requires x86. + run: pnpm run test:node-suites --shard=${{ matrix.shard }}/2 --exclude '**/tracing/tedious/**' job_build_tarballs: name: Build tarballs @@ -990,7 +1031,7 @@ jobs: always() && needs.job_build.result == 'success' needs: [job_get_metadata, job_build] - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -1007,7 +1048,7 @@ jobs: path: | .nx/cache .nx/workspace-data - key: ${{ env.NX_CACHE_KEY }} + key: ${{ env.NX_CACHE_KEY }}-${{ runner.arch }} - name: Restore caches uses: ./.github/actions/restore-cache @@ -1017,7 +1058,7 @@ jobs: - name: Build tarballs run: | pnpm run clean:tarballs - pnpm run build:tarballs --parallel=8 + pnpm run build:tarballs --parallel=4 - name: Upload tarball artifacts uses: actions/upload-artifact@v7 @@ -1048,7 +1089,7 @@ jobs: if: always() && needs.job_build_tarballs.result == 'success' && needs.job_build.outputs.e2e-matrix-1 != '{"include":[]}' - needs: [job_get_metadata, job_build, job_build_layer, job_build_tarballs] + needs: [job_get_metadata, job_build, job_build_tarballs] runs-on: ubuntu-24.04 timeout-minutes: 15 # We just use a dummy DSN here, only send to the tunnel anyhow @@ -1120,7 +1161,7 @@ jobs: if: always() && needs.job_build_tarballs.result == 'success' && needs.job_build.outputs.e2e-matrix-2 != '{"include":[]}' - needs: [job_get_metadata, job_build, job_build_layer, job_build_tarballs] + needs: [job_get_metadata, job_build, job_build_tarballs] runs-on: ubuntu-24.04 timeout-minutes: 15 env: *e2e-env @@ -1129,6 +1170,20 @@ jobs: matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix-2) }} steps: *e2e-steps + job_e2e_aws_layer_tests: + name: E2E ${{ matrix.label || matrix.test-application }} Test + needs: [job_get_metadata, job_build, job_build_layer, job_build_tarballs] + if: >- + always() && needs.job_build_tarballs.result == 'success' && needs.job_build_layer.result == 'success' && + needs.job_build.outputs.e2e-matrix-aws-layer != '{"include":[]}' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: *e2e-env + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix-aws-layer) }} + steps: *e2e-steps + # - We skip optional tests on release branches # - The tests run in optional-e2e.yml, so their failures do not mark the PR as failed. This job only waits for # them. optional-e2e.yml reports the results in a PR comment. @@ -1182,6 +1237,7 @@ jobs: job_deno_unit_tests, job_node_unit_tests, job_node_integration_tests, + job_sql_server_integration_tests, job_cloudflare_integration_tests, job_bundler_plugin_integration_tests, job_bun_integration_tests, @@ -1190,6 +1246,7 @@ jobs: job_browser_loader_tests, job_e2e_tests_1, job_e2e_tests_2, + job_e2e_aws_layer_tests, job_artifacts, job_lint, job_check_lockfile, diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3624e80c8a..cae8ddfbdbb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,133 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 11.12.0 + +### Important Changes + +- **feat(bundler-plugins): Enable hidden source maps and remove them after upload ([#25637](https://github.com/getsentry/sentry-javascript/pull/25637), [#25645](https://github.com/getsentry/sentry-javascript/pull/25645), [#25646](https://github.com/getsentry/sentry-javascript/pull/25646), [#25647](https://github.com/getsentry/sentry-javascript/pull/25647), [#25648](https://github.com/getsentry/sentry-javascript/pull/25648))** + + The Sentry Rollup, Vite, webpack and esbuild plugins now turn on hidden source maps when your bundler has no source maps enabled and the plugin can upload them. They upload the maps to Sentry and remove them from the build output. You no longer need to set `sourcemap: 'hidden'` or `filesToDeleteAfterUpload` globs: + + ```js + sentryVitePlugin({ org, project, authToken }); + ``` + + The new `sourcemaps.deleteSourcemapsAfterUpload` option controls the removal: + - Unset: the plugin removes only the source maps it turned on itself. Source maps you enabled yourself stay in the build output, as before. + - `true`: the plugin also removes source maps you enabled yourself, and switches visible source maps to hidden. Set this if you already use `sourcemap: 'hidden'` and want to drop your `filesToDeleteAfterUpload` globs. + - `false`: the plugin never removes source maps. + + Builds in development mode or without an auth token, org and project do not change. + +- **feat(cloudflare): Propagate tracing context through Queue messages ([#25306](https://github.com/getsentry/sentry-javascript/pull/25306))** + + Cloudflare Queue consumers can now link to the traces of the producers that sent the messages. Set `queueTracePropagationBindings` to the producer bindings that should add trace data to plain-object message bodies. Consumers remove this data before your handler runs, and the `queue.process` span links to every producer in the batch. + + ```ts + export default Sentry.withSentry( + env => ({ + dsn: env.SENTRY_DSN, + queueTracePropagationBindings: ['BILLING_QUEUE', /^JOBS_/], + }), + handler, + ); + ``` + +- **feat(core): Answer managed tunnel requests in the runtime ([#25997](https://github.com/getsentry/sentry-javascript/pull/25997), [#25998](https://github.com/getsentry/sentry-javascript/pull/25998), [#25999](https://github.com/getsentry/sentry-javascript/pull/25999), [#26000](https://github.com/getsentry/sentry-javascript/pull/26000), [#26001](https://github.com/getsentry/sentry-javascript/pull/26001))** + + `serveTunnelIntegration()` now answers tunnel requests before they reach your app, in Node.js, Bun, Deno, Cloudflare Workers and AWS Lambda. You no longer need a framework integration or a route handler, so any server setup works with only the integration: + + ```ts + Sentry.init({ dsn, integrations: [Sentry.serveTunnelIntegration()] }); + ``` + + To answer tunnel requests from your own code, set `serveTunnelIntegration({ intercept: false })` and call `handleTunnelRequestIfMatched`. + +### Other Changes + +- feat(server-utils): Always warn on Bun when no channel injection is detected ([#25857](https://github.com/getsentry/sentry-javascript/pull/25857)) +- feat(server-utils): Group the tool spans of an Anthropic session runner under one invoke_agent span ([#25995](https://github.com/getsentry/sentry-javascript/pull/25995)) +- feat(tanstackstart-react): Deprecate `tunnelRoute` ([#26225](https://github.com/getsentry/sentry-javascript/pull/26225)) +- fix(aws-serverless): Fork isolation scope per invocation ([#26191](https://github.com/getsentry/sentry-javascript/pull/26191)) +- fix(aws-serverless): Omit default port from url.full on function.aws spans ([#26227](https://github.com/getsentry/sentry-javascript/pull/26227)) +- fix(browser): Keep the current origin for `//` paths in `url.full` ([#26165](https://github.com/getsentry/sentry-javascript/pull/26165)) +- fix(cloudflare): Capture NonRetryableError before the final workflow step attempt ([#26161](https://github.com/getsentry/sentry-javascript/pull/26161)) +- fix(cloudflare): Initialize the SDK before constructing a Durable Object ([#26218](https://github.com/getsentry/sentry-javascript/pull/26218)) +- fix(cloudflare): Keep framework-managed methods when a Durable Object constructor throws ([#25851](https://github.com/getsentry/sentry-javascript/pull/25851)) +- fix(core): Avoid stack overflow in FunctionToString on cyclic Proxy prototype chains ([#26162](https://github.com/getsentry/sentry-javascript/pull/26162)) +- fix(core): Preserve parentheses in anonymous Node stack paths ([#26216](https://github.com/getsentry/sentry-javascript/pull/26216)) +- fix(core): stop normalize() from throwing on symbol and null-prototype errors ([#25967](https://github.com/getsentry/sentry-javascript/pull/25967)) +- fix(core): Use the final segment span name for streamed spans that end before a rename ([#26178](https://github.com/getsentry/sentry-javascript/pull/26178)) +- fix(deps): Bump the sentry CLI to ^0.49.0 ([#26200](https://github.com/getsentry/sentry-javascript/pull/26200)) +- fix(esbuild): Avoid import-is-undefined warning for entries without a default export ([#26132](https://github.com/getsentry/sentry-javascript/pull/26132)) +- fix(hono): Name routes without c.req.matchedRoutes ([#25454](https://github.com/getsentry/sentry-javascript/pull/25454)) +- fix(nextjs,sveltekit): Hide git revision console on Windows ([#26223](https://github.com/getsentry/sentry-javascript/pull/26223)) +- fix(node): Patch the mutable fs object in fsIntegration under native ESM ([#26086](https://github.com/getsentry/sentry-javascript/pull/26086)) +- fix(node): Read syncBuiltinESMExports off the module namespace so Deno can load @sentry/node ([#26155](https://github.com/getsentry/sentry-javascript/pull/26155)) +- fix(server-utils): Count Bedrock prompt-cache tokens as input tokens ([#26147](https://github.com/getsentry/sentry-javascript/pull/26147)) +- fix(server-utils): Create an invoke_agent span for LangGraph stream() ([#25749](https://github.com/getsentry/sentry-javascript/pull/25749)) +- fix(server-utils): End Bedrock stream spans with an error status on failure ([#26131](https://github.com/getsentry/sentry-javascript/pull/26131)) +- fix(server-utils): Keep Bedrock InvokeModel spans for byte, non-JSON, or model-less bodies ([#26124](https://github.com/getsentry/sentry-javascript/pull/26124)) +- fix(server-utils): Mark AI streaming spans as errored when the stream fails midway ([#26023](https://github.com/getsentry/sentry-javascript/pull/26023)) +- fix(server-utils): Read Bedrock Claude stream output tokens from message_delta ([#26170](https://github.com/getsentry/sentry-javascript/pull/26170)) +- fix(server-utils): Read Bedrock Mistral and Cohere stream token counts from invocation metrics ([#26176](https://github.com/getsentry/sentry-javascript/pull/26176)) +- fix(server-utils): Record Anthropic system instructions from text blocks ([#26182](https://github.com/getsentry/sentry-javascript/pull/26182)) +- fix(server-utils): Record Bedrock finish reasons as a JSON string ([#26184](https://github.com/getsentry/sentry-javascript/pull/26184)) +- fix(server-utils): Record LangGraph finish reasons as a JSON string ([#26185](https://github.com/getsentry/sentry-javascript/pull/26185)) +- fix(server-utils): Record Vercel AI system instructions from message objects ([#26181](https://github.com/getsentry/sentry-javascript/pull/26181)) +- fix(sveltekit): Read navigation untracked in the Kit 3 fetch wrapper ([#26136](https://github.com/getsentry/sentry-javascript/pull/26136)) + +
+ Internal Changes + +- chore(deps): Bump @modelcontextprotocol/server and client to 2.4.0 ([#26116](https://github.com/getsentry/sentry-javascript/pull/26116)) +- chore(deps): Bump `@sentry/conventions` to 0.31.0 ([#26160](https://github.com/getsentry/sentry-javascript/pull/26160)) +- chore(deps): Bump `@sentry/conventions` to 0.32.0 ([#26222](https://github.com/getsentry/sentry-javascript/pull/26222)) +- chore(deps): Bump body-parser to fix CVE-2026-111181 ([#26164](https://github.com/getsentry/sentry-javascript/pull/26164)) +- chore(deps): Bump dependencies to fix in-range security alerts ([#26221](https://github.com/getsentry/sentry-javascript/pull/26221)) +- chore(deps): Bump nanoid to fix CVE-2026-73086 ([#26173](https://github.com/getsentry/sentry-javascript/pull/26173)) +- chore(deps): Bump nx from 22.7.10 to 24.0.3 ([#26110](https://github.com/getsentry/sentry-javascript/pull/26110)) +- chore(deps): Bump path-to-regexp to fix CVE-2026-4867 ([#26174](https://github.com/getsentry/sentry-javascript/pull/26174)) +- chore(deps): Bump rrweb to 2.46.0 ([#26241](https://github.com/getsentry/sentry-javascript/pull/26241)) +- chore(deps): Bump vite to 6.4.5 and 7.3.7 ([#26113](https://github.com/getsentry/sentry-javascript/pull/26113)) +- chore(deps): Remove unused dependencies from workspace packages ([#26230](https://github.com/getsentry/sentry-javascript/pull/26230)) +- chore(deps-dev): Bump vite from 8.3.5 to 8.3.6 ([#26243](https://github.com/getsentry/sentry-javascript/pull/26243)) +- chore: use native `node:fs` for removal scripts ([#26219](https://github.com/getsentry/sentry-javascript/pull/26219)) +- docs: Point Sentry Conventions links to conventions.sentry.dev ([#26156](https://github.com/getsentry/sentry-javascript/pull/26156)) +- feat(deps): Bump ws from 8.18.0 to 8.22.2 ([#26242](https://github.com/getsentry/sentry-javascript/pull/26242)) +- fix(ci): refresh lockfile deprecation metadata ([#26183](https://github.com/getsentry/sentry-javascript/pull/26183)) +- perf(ci): (more) sharding of browser and runtime integration tests ([#26163](https://github.com/getsentry/sentry-javascript/pull/26163)) +- perf(ci): cache WebKit system package downloads ([#26198](https://github.com/getsentry/sentry-javascript/pull/26198)) +- perf(ci): combine circular dependency checks across entrypoints ([#26195](https://github.com/getsentry/sentry-javascript/pull/26195)) +- perf(ci): decouple ordinary E2E tests from the AWS Lambda layer build ([#26167](https://github.com/getsentry/sentry-javascript/pull/26167)) +- perf(ci): isolate SQL Server tests and use ARM integration runners ([#26148](https://github.com/getsentry/sentry-javascript/pull/26148)) +- perf(ci): minimize Playwright system dependency installation ([#26159](https://github.com/getsentry/sentry-javascript/pull/26159)) +- perf(ci): use ARM runners for builds and selected tests ([#26106](https://github.com/getsentry/sentry-javascript/pull/26106)) +- perf(ci): Use pnpm pack for tarballs ([#26024](https://github.com/getsentry/sentry-javascript/pull/26024)) +- ref(core): Set up log and metric flushing on first capture ([#26146](https://github.com/getsentry/sentry-javascript/pull/26146)) +- ref(server-utils): Add shared isBun, isDeno and isCloudflareWorkers helpers ([#26111](https://github.com/getsentry/sentry-javascript/pull/26111)) +- test(aws-serverless): Add Bedrock streaming integration scenario ([#26190](https://github.com/getsentry/sentry-javascript/pull/26190)) +- test(browser): Trigger third-party errors after initialization ([#26206](https://github.com/getsentry/sentry-javascript/pull/26206)) +- test(browser): Use navigation-aware back actions in bfcache tests ([#26208](https://github.com/getsentry/sentry-javascript/pull/26208)) +- test(bundler-plugins): Add integration tests for `sourcemaps.assets` and `sourcemaps.ignore` ([#25916](https://github.com/getsentry/sentry-javascript/pull/25916)) +- test(bundler-plugins): Increase timeout for webpack release-injection test ([#26172](https://github.com/getsentry/sentry-javascript/pull/26172)) +- test(cloudflare): Poll Workflow send aborts in the run context ([#26207](https://github.com/getsentry/sentry-javascript/pull/26207)) +- test(cloudflare): tolerate span timestamp rounding ([#26196](https://github.com/getsentry/sentry-javascript/pull/26196)) +- test(e2e): Fix cloudflare-pi-durable builds ([#26235](https://github.com/getsentry/sentry-javascript/pull/26235)) +- test(e2e): Pin elysia to v1 in the elysia-node test app ([#26215](https://github.com/getsentry/sentry-javascript/pull/26215)) +- test(e2e): Remove unused dependencies from E2E test apps ([#26231](https://github.com/getsentry/sentry-javascript/pull/26231)) +- test(hono): Fix flaky middleware error status test ([#26213](https://github.com/getsentry/sentry-javascript/pull/26213)) +- test(hono): relax middleware span timing assertions ([#26199](https://github.com/getsentry/sentry-javascript/pull/26199)) +- test(node): collect TypeSafe spans across streamed envelopes ([#26151](https://github.com/getsentry/sentry-javascript/pull/26151)) +- test(nuxt): Keep Nitro 2 WebSocket handlers resolvable with the managed tunnel ([#26082](https://github.com/getsentry/sentry-javascript/pull/26082)) +- test(react): Synchronize lazy-route navigation cancellation ([#26210](https://github.com/getsentry/sentry-javascript/pull/26210)) +- test(replay): Collect request spans across recording segments ([#26209](https://github.com/getsentry/sentry-javascript/pull/26209)) + +
+ +Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, @chan-mai, @nuntz, and @43081j. Thank you for your contributions! + ## 11.11.0 ### Important Changes diff --git a/MIGRATION.md b/MIGRATION.md index 9785e86fd6a8..9bde06ab53f1 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1013,7 +1013,7 @@ The process still exits with code `1` and the reason is still written to `stderr Affected SDKs: All SDKs. -With [span streaming](#span-streaming-is-now-the-default) enabled (the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/). +With [span streaming](#span-streaming-is-now-the-default) enabled (the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://conventions.sentry.dev/names/). If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged. diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 76d4fcc337ce..1cf35c855bd6 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -8,7 +8,7 @@ }, "private": true, "scripts": { - "clean": "rimraf -g suites/**/dist loader-suites/**/dist tmp", + "clean": "node scripts/clean.mjs", "install-browsers": "[[ -z \"$SKIP_PLAYWRIGHT_BROWSER_INSTALL\" ]] && npx playwright install --with-deps || echo 'Skipping browser installation'", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", @@ -61,12 +61,12 @@ "@playwright/test": "1.64.1", "@sentry/browser": "11.11.0", "@sentry/browser-utils": "11.11.0", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "11.11.0", "@sentry/replay": "11.11.0", "@sentry/replay-canvas": "11.11.0", - "@sentry/rrweb": "2.45.0", - "@sentry/rrweb-types": "^2.45.0", + "@sentry/rrweb": "2.46.0", + "@sentry/rrweb-types": "^2.46.0", "@sentry/wasm": "11.11.0", "@supabase/supabase-js": "2.109.0", "@types/node": "^20.19.0", diff --git a/dev-packages/browser-integration-tests/scripts/clean.mjs b/dev-packages/browser-integration-tests/scripts/clean.mjs new file mode 100644 index 000000000000..3538caf777d7 --- /dev/null +++ b/dev-packages/browser-integration-tests/scripts/clean.mjs @@ -0,0 +1,5 @@ +import { globSync, rmSync } from 'node:fs'; + +for (const path of globSync(['suites/**/dist', 'loader-suites/**/dist', 'tmp'])) { + rmSync(path, { recursive: true, force: true }); +} diff --git a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/template.html b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/template.html index 25a91142be08..6c815dc4de38 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/template.html +++ b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/template.html @@ -4,6 +4,7 @@ + diff --git a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/test.ts b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/test.ts index 9b918e8d1170..a523e3964d97 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/test.ts @@ -26,6 +26,7 @@ sentryTest('tags event if contains at least one third-party frame', async ({ get ); await page.goto(url); + await page.click('#thirdPartyErrBtn'); const errorEvent = envelopeRequestParser(await errorEventPromise); expect(errorEvent.tags?.third_party_code).toBe(true); diff --git a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/thirdPartyScript.js b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/thirdPartyScript.js index e6a2e35dba01..ba25706f6b98 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/thirdPartyScript.js +++ b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/thirdPartyScript.js @@ -1,3 +1,3 @@ -setTimeout(() => { +document.getElementById('thirdPartyErrBtn').addEventListener('click', () => { throw new Error('I am a third party Error'); -}, 100); +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/requests/test.ts b/dev-packages/browser-integration-tests/suites/replay/requests/test.ts index 9e0ee49ed58c..de9079a5dc17 100644 --- a/dev-packages/browser-integration-tests/suites/replay/requests/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/requests/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; import { expectedFetchPerformanceSpan, expectedXHRPerformanceSpan } from '../../../utils/replayEventTemplates'; -import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; +import { collectReplayRequests, getReplayPerformanceSpans, shouldSkipReplayTest } from '../../../utils/replayHelpers'; sentryTest('replay recording should contain fetch request span', async ({ getLocalTestUrl, page, browserName }) => { // Possibly related: https://github.com/microsoft/playwright/issues/11390 @@ -17,21 +17,19 @@ sentryTest('replay recording should contain fetch request span', async ({ getLoc }); }); - const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); + }); const url = await getLocalTestUrl({ testDir: __dirname }); - - const [req0] = await Promise.all([reqPromise0, page.goto(url), page.locator('#go-background').click()]); - - const { performanceSpans: spans0 } = getReplayRecordingContent(req0); + await page.goto(url); + await page.locator('#go-background').click(); await Promise.all([page.waitForResponse('https://sentry-test-site.example'), page.locator('#fetch').click()]); - const { performanceSpans: spans1 } = getReplayRecordingContent(await reqPromise1); - - const performanceSpans = [...spans0, ...spans1]; - expect(performanceSpans).toContainEqual(expectedFetchPerformanceSpan); + const { replayRecordingSnapshots } = await replayRequestPromise; + const requestSpans = getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.fetch'); + expect(requestSpans).toEqual([expectedFetchPerformanceSpan]); }); sentryTest('replay recording should contain XHR request span', async ({ getLocalTestUrl, page, browserName }) => { @@ -47,20 +45,17 @@ sentryTest('replay recording should contain XHR request span', async ({ getLocal }); }); - const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); + const replayRequestPromise = collectReplayRequests(page, recordingEvents => { + return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); + }); const url = await getLocalTestUrl({ testDir: __dirname }); - - const [req0] = await Promise.all([reqPromise0, page.goto(url), page.locator('#go-background').click()]); - - const { performanceSpans: spans0 } = getReplayRecordingContent(req0); + await page.goto(url); + await page.locator('#go-background').click(); await Promise.all([page.waitForResponse('https://sentry-test-site.example'), page.locator('#xhr').click()]); - const { performanceSpans: spans1 } = getReplayRecordingContent(await reqPromise1); - - const performanceSpans = [...spans0, ...spans1]; - - expect(performanceSpans).toContainEqual(expectedXHRPerformanceSpan); + const { replayRecordingSnapshots } = await replayRequestPromise; + const requestSpans = getReplayPerformanceSpans(replayRecordingSnapshots).filter(span => span.op === 'resource.xhr'); + expect(requestSpans).toEqual([expectedXHRPerformanceSpan]); }); diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 046e973354dd..60d47967bd65 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -27,6 +27,7 @@ const NO_HTTP_SERVER_SPANS = [ 'suites/tracing/httpServerSpans-unrouted/test.ts', 'suites/tracing/ignoreSpans/**', 'suites/tracing/managed-tunnel/test.ts', + 'suites/tunnel/hardening/test.ts', 'suites/tracing/meta-tags-twp-errors/test.ts', 'suites/tracing/meta-tags/test.ts', 'suites/tracing/requestData/test.ts', @@ -199,7 +200,13 @@ export const NODE_SUITES_EXCLUDE = ['**/node_modules/**', ...NODE_ONLY, ...NO_AU export const SENTRY_NODE_EXCLUDE = [...NO_HTTP_SERVER_SPANS, ...NO_FETCH_INSTRUMENTATION]; // Excluded only in the `node-suites-sentry-bun` project, which maps `@sentry/node` to `@sentry/bun`. +// Bun answers nothing on a connection whose first request awaits anything while a second one is +// queued behind it, which the pipelined cases need. `packages/bun` runs the same cases against Bun's +// servers without those. +const BUN_DROPS_PIPELINED_REQUESTS = ['suites/tunnel/hardening/test.ts']; + export const SENTRY_BUN_EXCLUDE = [ + ...BUN_DROPS_PIPELINED_REQUESTS, ...HTTP_SERVER_OPTIONS_IGNORED, ...NO_EXPRESS_INSTRUMENTATION, ...NO_NATIVE_NODE_FETCH_INTEGRATION, @@ -257,6 +264,8 @@ const BUN_BUILD_NOT_TRIAGED = [ export const BUN_BUILD_EXCLUDE = [ '**/node_modules/**', + // SQL Server runs in the dedicated Node/x86 CI job. + 'suites/tracing/tedious/test.ts', ...BUN_BUILD_EAGER_SUBSCRIPTION, ...BUN_BUILD_NO_HTTP_SERVER_INTEGRATION, ...BUN_BUILD_UNBUNDLEABLE_DEPENDENCY, diff --git a/dev-packages/bun-integration-tests/package.json b/dev-packages/bun-integration-tests/package.json index 0535ff2a987c..db35da187547 100644 --- a/dev-packages/bun-integration-tests/package.json +++ b/dev-packages/bun-integration-tests/package.json @@ -15,7 +15,6 @@ }, "devDependencies": { "@sentry-internal/node-integration-tests": "11.11.0", - "@sentry-internal/test-utils": "11.11.0", "@sentry/bun": "11.11.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", @@ -25,6 +24,6 @@ "hono": "^4.13.7", "mysql": "^2.18.1", "pg": "8.23.1", - "vitest": "^4.1.11" + "vitest": "^4.1.12" } } diff --git a/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts new file mode 100644 index 000000000000..d7f5b49d899f --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/bun'; + +Sentry.initWithoutDefaultIntegrations({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts new file mode 100644 index 000000000000..0ce01f05a57b --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/bun'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, +}); diff --git a/dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts b/dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts new file mode 100644 index 000000000000..0a8953ea3aba --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts @@ -0,0 +1,22 @@ +import { spawnSync } from 'child_process'; +import { join } from 'path'; +import { describe, expect, it } from 'vitest'; + +function runScenario(file: string): string { + const res = spawnSync('bun', ['run', join(__dirname, file)], { cwd: __dirname, encoding: 'utf8' }); + expect(res.status, `run failed:\nstderr:\n${res.stderr}\nstdout:\n${res.stdout}`).toBe(0); + return `${res.stdout}${res.stderr}`; +} + +describe('no channel injection (Bun)', () => { + it('warns without `debug` when the app runs unbuilt with `bun run`', () => { + const output = runScenario('scenario.ts'); + + expect(output).toContain('No diagnostics-channel injection detected'); + expect(output).toContain('@sentry/bun/plugin'); + }); + + it('does not warn when no channel integration is installed', () => { + expect(runScenario('scenario-no-integrations.ts')).not.toContain('No diagnostics-channel injection detected'); + }); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.d.ts new file mode 100644 index 000000000000..192b614bc29b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.d.ts @@ -0,0 +1,2 @@ +import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup"; +export declare const sentryConfig: SentryRollupPluginOptions; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.js new file mode 100644 index 000000000000..4013b86e95cf --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.js @@ -0,0 +1,7 @@ +export const sentryConfig = { + telemetry: false, + authToken: "fake-auth", + org: "fake-org", + project: "fake-project", + release: { inject: false }, +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.d.ts new file mode 100644 index 000000000000..192b614bc29b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.d.ts @@ -0,0 +1,2 @@ +import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup"; +export declare const sentryConfig: SentryRollupPluginOptions; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.js new file mode 100644 index 000000000000..fd2a5648a631 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.js @@ -0,0 +1,10 @@ +export const sentryConfig = { + telemetry: false, + authToken: "fake-auth", + org: "fake-org", + project: "fake-project", + release: { inject: false }, + sourcemaps: { + assets: ["out/sourcemaps-assets-auto/entry1.js"], + }, +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.d.ts new file mode 100644 index 000000000000..192b614bc29b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.d.ts @@ -0,0 +1,2 @@ +import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup"; +export declare const sentryConfig: SentryRollupPluginOptions; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.js new file mode 100644 index 000000000000..a78ae93f7ec1 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.js @@ -0,0 +1,10 @@ +export const sentryConfig = { + telemetry: false, + authToken: "fake-auth", + org: "fake-org", + project: "fake-project", + release: { inject: false }, + sourcemaps: { + assets: ["out/sourcemaps-assets/entry1.js"], + }, +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.d.ts new file mode 100644 index 000000000000..192b614bc29b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.d.ts @@ -0,0 +1,2 @@ +import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup"; +export declare const sentryConfig: SentryRollupPluginOptions; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.js new file mode 100644 index 000000000000..2c6890272dfd --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.js @@ -0,0 +1,10 @@ +export const sentryConfig = { + telemetry: false, + authToken: "fake-auth", + org: "fake-org", + project: "fake-project", + release: { inject: false }, + sourcemaps: { + ignore: ["**/entry2.js"], + }, +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.d.ts new file mode 100644 index 000000000000..192b614bc29b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.d.ts @@ -0,0 +1,2 @@ +import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup"; +export declare const sentryConfig: SentryRollupPluginOptions; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.js new file mode 100644 index 000000000000..2c6890272dfd --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.js @@ -0,0 +1,10 @@ +export const sentryConfig = { + telemetry: false, + authToken: "fake-auth", + org: "fake-org", + project: "fake-project", + release: { inject: false }, + sourcemaps: { + ignore: ["**/entry2.js"], + }, +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.config.js new file mode 100644 index 000000000000..59ca69b920d6 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.config.js @@ -0,0 +1,15 @@ +import * as esbuild from "esbuild"; +import { sentryEsbuildPlugin } from "@sentry/bundler-plugins/esbuild"; +import { sentryConfig } from "../configs/sourcemaps-assets.config.js"; + +await esbuild.build({ + entryPoints: ["./src/entry1.js", "./src/entry2.js"], + bundle: true, + outdir: "./out/sourcemaps-assets", + minify: false, + format: "esm", + splitting: true, + chunkNames: "[name]", + sourcemap: true, + plugins: [sentryEsbuildPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.test.ts new file mode 100644 index 000000000000..f16619de3be6 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.test.ts @@ -0,0 +1,36 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "chunk.js", + "chunk.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "sentry-debug-id-stub:_sentry-debug-id-injection-stub?sentry-module-id=00000000-0000-0000-0000-000000000000", + "../../src/entry1.js", + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.config.js new file mode 100644 index 000000000000..7ecbc3a35e0e --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.config.js @@ -0,0 +1,15 @@ +import * as esbuild from "esbuild"; +import { sentryEsbuildPlugin } from "@sentry/bundler-plugins/esbuild"; +import { sentryConfig } from "../configs/sourcemaps-ignore.config.js"; + +await esbuild.build({ + entryPoints: ["./src/entry1.js", "./src/entry2.js"], + bundle: true, + outdir: "./out/sourcemaps-ignore", + minify: false, + format: "esm", + splitting: true, + chunkNames: "[name]", + sourcemap: true, + plugins: [sentryEsbuildPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.test.ts new file mode 100644 index 000000000000..f16619de3be6 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.test.ts @@ -0,0 +1,36 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "chunk.js", + "chunk.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "sentry-debug-id-stub:_sentry-debug-id-injection-stub?sentry-module-id=00000000-0000-0000-0000-000000000000", + "../../src/entry1.js", + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts index b648cee7c415..9b23588858ac 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts index b648cee7c415..9b23588858ac 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.config.ts new file mode 100644 index 000000000000..f8e7026e7182 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.config.ts @@ -0,0 +1,23 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rolldown"; +import { sentryConfig } from "../configs/multiple-outputs-mixed-sourcemaps.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: [ + { + dir: "out/multiple-outputs-mixed-sourcemaps", + format: "es", + entryFileNames: "[name].mjs", + chunkFileNames: "[name].mjs", + sourcemap: true, + }, + { + dir: "out/multiple-outputs-mixed-sourcemaps", + format: "cjs", + entryFileNames: "[name].cjs", + chunkFileNames: "[name].cjs", + }, + ], + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.test.ts new file mode 100644 index 000000000000..3e987dc46dc3 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.test.ts @@ -0,0 +1,35 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler(); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.cjs", + "common.mjs", + "common.mjs.map", + "entry1.cjs", + "entry1.mjs", + "entry1.mjs.map", + "entry2.cjs", + "entry2.mjs", + "entry2.mjs.map", + "sentry-cli-mock.json", + ] + `); + // The two outputs write at the same time, so the order of the CLI calls varies. + expect(files["sentry-cli-mock.json"]!.trim().split("\n").sort()).toMatchInlineSnapshot(` + [ + "["release","create","CURRENT_SHA","--project","fake-project"],", + "["release","create","CURRENT_SHA","--project","fake-project"],", + "["release","finalize","CURRENT_SHA"],", + "["release","finalize","CURRENT_SHA"],", + "["release","set-commits","CURRENT_SHA","--auto"],", + "["release","set-commits","CURRENT_SHA","--auto"],", + "["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],", + "["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],", + ] + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts index cd5480b81387..44729ef41b57 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.config.ts new file mode 100644 index 000000000000..ee6f81fe2225 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.config.ts @@ -0,0 +1,12 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rolldown"; +import { sentryConfig } from "../configs/sourcemaps-assets-auto.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-assets-auto", + chunkFileNames: "[name].js", + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.test.ts new file mode 100644 index 000000000000..4a8f0fc7754f --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.test.ts @@ -0,0 +1,31 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.config.ts new file mode 100644 index 000000000000..6990dda586fa --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.config.ts @@ -0,0 +1,13 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rolldown"; +import { sentryConfig } from "../configs/sourcemaps-assets.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-assets", + chunkFileNames: "[name].js", + sourcemap: true, + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.test.ts new file mode 100644 index 000000000000..af148b26891b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.test.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "common.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.config.ts new file mode 100644 index 000000000000..f5b82becb477 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.config.ts @@ -0,0 +1,12 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rolldown"; +import { sentryConfig } from "../configs/sourcemaps-ignore-auto.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-ignore-auto", + chunkFileNames: "[name].js", + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.test.ts new file mode 100644 index 000000000000..3d78ed2d1c45 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.test.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/common.js", + ], + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.config.ts new file mode 100644 index 000000000000..0cb7600ca4db --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.config.ts @@ -0,0 +1,13 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rolldown"; +import { sentryConfig } from "../configs/sourcemaps-ignore.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-ignore", + chunkFileNames: "[name].js", + sourcemap: true, + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.test.ts new file mode 100644 index 000000000000..d320a622a424 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.test.ts @@ -0,0 +1,37 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "common.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/common.js", + ], + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts index 85b1040db223..377c85d97b9a 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts @@ -10,6 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts index 1a1d23690e7d..6295b31445f3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts @@ -11,6 +11,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.config.js new file mode 100644 index 000000000000..db32173f1382 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.config.js @@ -0,0 +1,23 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rollup"; +import { sentryConfig } from "../configs/multiple-outputs-mixed-sourcemaps.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: [ + { + dir: "out/multiple-outputs-mixed-sourcemaps", + format: "es", + entryFileNames: "[name].mjs", + chunkFileNames: "[name].mjs", + sourcemap: true, + }, + { + dir: "out/multiple-outputs-mixed-sourcemaps", + format: "cjs", + entryFileNames: "[name].cjs", + chunkFileNames: "[name].cjs", + }, + ], + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.test.ts new file mode 100644 index 000000000000..3e987dc46dc3 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.test.ts @@ -0,0 +1,35 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler(); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.cjs", + "common.mjs", + "common.mjs.map", + "entry1.cjs", + "entry1.mjs", + "entry1.mjs.map", + "entry2.cjs", + "entry2.mjs", + "entry2.mjs.map", + "sentry-cli-mock.json", + ] + `); + // The two outputs write at the same time, so the order of the CLI calls varies. + expect(files["sentry-cli-mock.json"]!.trim().split("\n").sort()).toMatchInlineSnapshot(` + [ + "["release","create","CURRENT_SHA","--project","fake-project"],", + "["release","create","CURRENT_SHA","--project","fake-project"],", + "["release","finalize","CURRENT_SHA"],", + "["release","finalize","CURRENT_SHA"],", + "["release","set-commits","CURRENT_SHA","--auto"],", + "["release","set-commits","CURRENT_SHA","--auto"],", + "["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],", + "["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"],", + ] + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts index 85b1040db223..377c85d97b9a 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts @@ -10,6 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.config.js new file mode 100644 index 000000000000..5ea2b86e6cea --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.config.js @@ -0,0 +1,12 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rollup"; +import { sentryConfig } from "../configs/sourcemaps-assets-auto.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-assets-auto", + chunkFileNames: "[name].js", + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.test.ts new file mode 100644 index 000000000000..4a8f0fc7754f --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.test.ts @@ -0,0 +1,31 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.config.js new file mode 100644 index 000000000000..fdd3564869a6 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.config.js @@ -0,0 +1,13 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rollup"; +import { sentryConfig } from "../configs/sourcemaps-assets.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-assets", + chunkFileNames: "[name].js", + sourcemap: true, + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.test.ts new file mode 100644 index 000000000000..af148b26891b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.test.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "common.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.config.js new file mode 100644 index 000000000000..a71f1098f713 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.config.js @@ -0,0 +1,12 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rollup"; +import { sentryConfig } from "../configs/sourcemaps-ignore-auto.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-ignore-auto", + chunkFileNames: "[name].js", + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.test.ts new file mode 100644 index 000000000000..3d78ed2d1c45 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.test.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/common.js", + ], + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.config.js new file mode 100644 index 000000000000..d4fb120db1c0 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.config.js @@ -0,0 +1,13 @@ +import { sentryRollupPlugin } from "@sentry/bundler-plugins/rollup"; +import { defineConfig } from "rollup"; +import { sentryConfig } from "../configs/sourcemaps-ignore.config.js"; + +export default defineConfig({ + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-ignore", + chunkFileNames: "[name].js", + sourcemap: true, + }, + plugins: [sentryRollupPlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.test.ts new file mode 100644 index 000000000000..d320a622a424 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.test.ts @@ -0,0 +1,37 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "common.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/common.js", + ], + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs index cd449620c86d..fd2c7a2bbb3f 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs @@ -27,6 +27,25 @@ function record(args) { fs.appendFileSync(out, `${JSON.stringify(args)},\n`); } +/** + * Write the `sources` of every source map in the upload directory to `sentry-uploaded-sources.json`. + * The plugin renames uploaded files to `-.js`, so only the `sources` tell which + * chunks were uploaded. Opt-in through `SENTRY_TEST_RECORD_UPLOADED_SOURCES` so other snapshots + * stay unchanged. + */ +function recordUploadedSources(directory) { + const outDir = process.env['SENTRY_TEST_OUT_DIR']; + if (!outDir || !process.env['SENTRY_TEST_RECORD_UPLOADED_SOURCES'] || !fs.existsSync(directory)) { + return; + } + const sources = fs + .readdirSync(directory) + .filter(file => file.endsWith('.map')) + .map(file => JSON.parse(fs.readFileSync(path.join(directory, file), 'utf8')).sources) + .sort((a, b) => String(a).localeCompare(String(b))); + fs.writeFileSync(path.join(outDir, 'sentry-uploaded-sources.json'), `${JSON.stringify(sources, null, 2)}\n`); +} + /** Append boolean flags (only when true) and value flags (only when set) to an args array. */ function pushFlags(args, flags, spec) { for (const [key, flag] of Object.entries(spec)) { @@ -83,6 +102,7 @@ function createSentrySDK(options = {}) { pushFlags(args, params, { release: '--release', dist: '--dist' }); if (params.directory) { args.push(params.directory); + recordUploadedSources(params.directory); } pushFlags(args, params, { ext: '--ext', diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts index 06d3ccb3dccd..1c17c400cc29 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts index 06d3ccb3dccd..1c17c400cc29 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts index 36d836956df6..8a6ce51205b1 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts @@ -18,6 +18,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts index 29e892eae928..c043197a4ac9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts index 29e892eae928..c043197a4ac9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts index a6e4dfac1179..eb67721a1582 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts @@ -18,6 +18,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts index b648cee7c415..9b23588858ac 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts index b648cee7c415..9b23588858ac 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts @@ -20,6 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts index cd5480b81387..44729ef41b57 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts @@ -19,6 +19,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.config.ts new file mode 100644 index 000000000000..858e86193cfa --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.config.ts @@ -0,0 +1,18 @@ +import { sentryVitePlugin } from "@sentry/bundler-plugins/vite"; +import { defineConfig } from "vite"; +import { sentryConfig } from "../configs/sourcemaps-assets-auto.config.js"; + +export default defineConfig({ + build: { + minify: false, + rollupOptions: { + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-assets-auto", + chunkFileNames: "[name].js", + entryFileNames: "[name].js", + }, + }, + }, + plugins: [sentryVitePlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.test.ts new file mode 100644 index 000000000000..4a8f0fc7754f --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.test.ts @@ -0,0 +1,31 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.config.ts new file mode 100644 index 000000000000..cbb96a3b4f7b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.config.ts @@ -0,0 +1,19 @@ +import { sentryVitePlugin } from "@sentry/bundler-plugins/vite"; +import { defineConfig } from "vite"; +import { sentryConfig } from "../configs/sourcemaps-assets.config.js"; + +export default defineConfig({ + build: { + minify: false, + sourcemap: true, + rollupOptions: { + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-assets", + chunkFileNames: "[name].js", + entryFileNames: "[name].js", + }, + }, + }, + plugins: [sentryVitePlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.test.ts new file mode 100644 index 000000000000..af148b26891b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.test.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "common.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.config.ts new file mode 100644 index 000000000000..a1e289918d56 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.config.ts @@ -0,0 +1,18 @@ +import { sentryVitePlugin } from "@sentry/bundler-plugins/vite"; +import { defineConfig } from "vite"; +import { sentryConfig } from "../configs/sourcemaps-ignore-auto.config.js"; + +export default defineConfig({ + build: { + minify: false, + rollupOptions: { + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-ignore-auto", + chunkFileNames: "[name].js", + entryFileNames: "[name].js", + }, + }, + }, + plugins: [sentryVitePlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.test.ts new file mode 100644 index 000000000000..3d78ed2d1c45 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.test.ts @@ -0,0 +1,34 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/common.js", + ], + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.config.ts new file mode 100644 index 000000000000..54fad01a9a8b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.config.ts @@ -0,0 +1,19 @@ +import { sentryVitePlugin } from "@sentry/bundler-plugins/vite"; +import { defineConfig } from "vite"; +import { sentryConfig } from "../configs/sourcemaps-ignore.config.js"; + +export default defineConfig({ + build: { + minify: false, + sourcemap: true, + rollupOptions: { + input: ["src/entry1.js", "src/entry2.js"], + output: { + dir: "out/sourcemaps-ignore", + chunkFileNames: "[name].js", + entryFileNames: "[name].js", + }, + }, + }, + plugins: [sentryVitePlugin(sentryConfig)], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.test.ts new file mode 100644 index 000000000000..d320a622a424 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.test.ts @@ -0,0 +1,37 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "common.js", + "common.js.map", + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "../../src/common.js", + ], + [ + "../../src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts index 6228a54dc2af..86863c2af2f7 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts @@ -15,6 +15,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts index 6228a54dc2af..86863c2af2f7 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts @@ -15,6 +15,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts index 08320b93d43a..ac55021950dc 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts @@ -14,6 +14,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { ;", "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.config.js new file mode 100644 index 000000000000..7752c8c253c9 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.config.js @@ -0,0 +1,20 @@ +import { sentryWebpackPlugin } from "@sentry/bundler-plugins/webpack"; +import { sentryConfig } from "../configs/sourcemaps-assets-auto.config.js"; +import { resolve } from "node:path"; + +export default { + cache: false, + entry: { + entry1: "./src/entry1.js", + entry2: "./src/entry2.js", + }, + output: { + path: resolve("./out/sourcemaps-assets-auto"), + filename: "[name].js", + }, + optimization: { + minimize: false, + }, + mode: "production", + plugins: [sentryWebpackPlugin(sentryConfig)], +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.test.ts new file mode 100644 index 000000000000..c303f183ad37 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.test.ts @@ -0,0 +1,31 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "webpack5-integration-tests/./src/common.js", + "webpack5-integration-tests/./src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.config.js new file mode 100644 index 000000000000..2b4b72ec6bc7 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.config.js @@ -0,0 +1,21 @@ +import { sentryWebpackPlugin } from "@sentry/bundler-plugins/webpack"; +import { sentryConfig } from "../configs/sourcemaps-assets.config.js"; +import { resolve } from "node:path"; + +export default { + cache: false, + entry: { + entry1: "./src/entry1.js", + entry2: "./src/entry2.js", + }, + output: { + path: resolve("./out/sourcemaps-assets"), + filename: "[name].js", + }, + devtool: "source-map", + optimization: { + minimize: false, + }, + mode: "production", + plugins: [sentryWebpackPlugin(sentryConfig)], +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.test.ts new file mode 100644 index 000000000000..24236bd8553a --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.test.ts @@ -0,0 +1,33 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "webpack5-integration-tests/./src/common.js", + "webpack5-integration-tests/./src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.config.js new file mode 100644 index 000000000000..5dfdca3dd827 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.config.js @@ -0,0 +1,20 @@ +import { sentryWebpackPlugin } from "@sentry/bundler-plugins/webpack"; +import { sentryConfig } from "../configs/sourcemaps-ignore-auto.config.js"; +import { resolve } from "node:path"; + +export default { + cache: false, + entry: { + entry1: "./src/entry1.js", + entry2: "./src/entry2.js", + }, + output: { + path: resolve("./out/sourcemaps-ignore-auto"), + filename: "[name].js", + }, + optimization: { + minimize: false, + }, + mode: "production", + plugins: [sentryWebpackPlugin(sentryConfig)], +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.test.ts new file mode 100644 index 000000000000..c303f183ad37 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.test.ts @@ -0,0 +1,31 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "entry1.js", + "entry2.js", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "webpack5-integration-tests/./src/common.js", + "webpack5-integration-tests/./src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.config.js new file mode 100644 index 000000000000..406ff5d7aaba --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.config.js @@ -0,0 +1,21 @@ +import { sentryWebpackPlugin } from "@sentry/bundler-plugins/webpack"; +import { sentryConfig } from "../configs/sourcemaps-ignore.config.js"; +import { resolve } from "node:path"; + +export default { + cache: false, + entry: { + entry1: "./src/entry1.js", + entry2: "./src/entry2.js", + }, + output: { + path: resolve("./out/sourcemaps-ignore"), + filename: "[name].js", + }, + devtool: "source-map", + optimization: { + minimize: false, + }, + mode: "production", + plugins: [sentryWebpackPlugin(sentryConfig)], +}; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.test.ts new file mode 100644 index 000000000000..24236bd8553a --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.test.ts @@ -0,0 +1,33 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, readOutputFiles }) => { + runBundler({ SENTRY_TEST_RECORD_UPLOADED_SOURCES: "1" }); + const files = readOutputFiles(); + + expect(Object.keys(files).sort()).toMatchInlineSnapshot(` + [ + "entry1.js", + "entry1.js.map", + "entry2.js", + "entry2.js.map", + "sentry-cli-mock.json", + "sentry-uploaded-sources.json", + ] + `); + expect(JSON.parse(files["sentry-uploaded-sources.json"]!)).toMatchInlineSnapshot(` + [ + [ + "webpack5-integration-tests/./src/common.js", + "webpack5-integration-tests/./src/entry1.js", + ], + ] + `); + expect(files["sentry-cli-mock.json"]).toMatchInlineSnapshot(` + "["release","create","CURRENT_SHA","--project","fake-project"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path"], + " + `); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/package.json b/dev-packages/bundler-plugin-integration-tests/package.json index 6fdddbb398bb..7969c5ea72d3 100644 --- a/dev-packages/bundler-plugin-integration-tests/package.json +++ b/dev-packages/bundler-plugin-integration-tests/package.json @@ -8,14 +8,13 @@ "check:types": "tsc --project ./tsconfig.json --noEmit", "clean": "run-s clean:build", "clean:all": "run-p clean clean:deps", - "clean:build": "premove ./fixtures/*/out", - "clean:deps": "premove node_modules" + "clean:build": "node ../../scripts/rm.mjs ./fixtures/*/out", + "clean:deps": "node ../../scripts/rm.mjs node_modules" }, "devDependencies": { "@sentry/bundler-plugins": "11.11.0", - "premove": "^4.0.0", - "sentry": "^0.45.0", + "sentry": "^0.49.0", "typescript": "~6.0.3", - "vitest": "^4.1.11" + "vitest": "^4.1.12" } } diff --git a/dev-packages/bundler-tests/package.json b/dev-packages/bundler-tests/package.json index 2a058ad4292a..06812323eeff 100644 --- a/dev-packages/bundler-tests/package.json +++ b/dev-packages/bundler-tests/package.json @@ -30,7 +30,7 @@ "@vercel/nft": "^1.11.0", "rollup": "^4.60.3", "vite": "^8.3.5", - "vitest": "^4.1.11", + "vitest": "^4.1.12", "webpack": "^5.112.2" }, "type": "module" diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 7e1160804461..1f7aa4671b1b 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -28,16 +28,15 @@ "@prisma/client": "6.15.0", "@sentry-internal/test-utils": "11.11.0", "@sentry/cloudflare": "11.11.0", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", "ai": "^6.0.0", "hono": "^4.13.7", - "langchain": "^1.5.16", "openai": "5.18.1", "prisma": "6.15.0", "vite": "^8.3.5", - "vitest": "^4.1.11", + "vitest": "^4.1.12", "wrangler": "4.156.0" } } diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts index 984c79480d68..aa2c0ee73dae 100644 --- a/dev-packages/cloudflare-integration-tests/runner.ts +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -149,6 +149,8 @@ type StartResult = { completed(): Promise; /** Every non-ignored envelope received so far, matched or not, for count assertions. */ getReceivedEnvelopes(): Envelope[]; + /** The port wrangler serves the worker on, once it is ready. */ + getPort(): Promise; makeRequest( method: 'get' | 'post', path: string, @@ -558,6 +560,9 @@ export function createRunner(...paths: string[]) { getReceivedEnvelopes: function (): Envelope[] { return receivedEnvelopes; }, + getPort: function (): Promise { + return workerPortPromise; + }, makeRequest: async function ( method: 'get' | 'post', path: string, diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts index 08d34656ee5b..dbfd2945ee1f 100644 --- a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts @@ -5,7 +5,7 @@ import { createRunner } from '../../../runner'; // Every envelope of both requests is expected, and `failOnUnexpected` fails the test on any other one. // All of them are sent after the constructor work ran, so an error or span from that work would arrive -// before the runner completes. +// before the runner completes. The only span from that work is the storage read, which has no parent. it('does not capture errors a Durable Object catches in work its constructor started', async ({ signal }) => { const runner = createRunner(__dirname) .unordered() @@ -18,6 +18,11 @@ it('does not capture errors a Durable Object catches in work its constructor sta expect(envelope[1][0][0].type).toBe('event'); expect((envelope[1][0][1] as Event).message).toBe('async-method ping: degraded'); }) + .expect(envelope => { + expect(envelope[1][0][0].type).toBe('span'); + const container = envelope[1][0][1] as SerializedStreamedSpanContainer; + expect(container.items.map(item => item.name)).toEqual(['durable_object_storage_get']); + }) .expectN(2, envelope => { expect(envelope[1][0][0].type).toBe('span'); const container = envelope[1][0][1] as SerializedStreamedSpanContainer; diff --git a/dev-packages/cloudflare-integration-tests/suites/flush-timeout/index.ts b/dev-packages/cloudflare-integration-tests/suites/flush-timeout/index.ts index 37be3907a11c..ef4d5b96f527 100644 --- a/dev-packages/cloudflare-integration-tests/suites/flush-timeout/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/flush-timeout/index.ts @@ -12,13 +12,17 @@ interface Env { // ingest that never answers. The run reports to SERVER_URL once the SDK has aborted one of those pending sends. export class IssueWorkflow extends WorkflowEntrypoint { async run(_event: WorkflowEvent, step: WorkflowStep): Promise { - const stepSendAborted = new Promise(resolve => (lastSend.onAbort = resolve)); + lastSend.aborted = false; for (let index = 0; index < 100; index++) { await step.do(`step-${index}`, async () => index); } - await stepSendAborted; + // The abort happens in the I/O context of a step. Resolving a promise of the run from there does not reliably + // wake the run, so the run polls with its own timer instead. + while (!lastSend.aborted) { + await new Promise(resolve => setTimeout(resolve, 50)); + } await fetch(`${this.env.SERVER_URL}/result`, { method: 'POST', body: JSON.stringify({ send: 'aborted' }) }); } } diff --git a/dev-packages/cloudflare-integration-tests/suites/flush-timeout/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/flush-timeout/instrument.server.ts index 3e8e2ff64d7b..f1f907859081 100644 --- a/dev-packages/cloudflare-integration-tests/suites/flush-timeout/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/flush-timeout/instrument.server.ts @@ -20,7 +20,6 @@ export default defineCloudflareOptions((env: Env) => ({ fetch: (input, init) => { init?.signal?.addEventListener('abort', () => { lastSend.aborted = true; - lastSend.onAbort?.(); }); return fetch(input, init); }, diff --git a/dev-packages/cloudflare-integration-tests/suites/flush-timeout/lastSend.ts b/dev-packages/cloudflare-integration-tests/suites/flush-timeout/lastSend.ts index 6c5cf6cfdaf8..f365a4dc6818 100644 --- a/dev-packages/cloudflare-integration-tests/suites/flush-timeout/lastSend.ts +++ b/dev-packages/cloudflare-integration-tests/suites/flush-timeout/lastSend.ts @@ -1,3 +1,2 @@ -// `aborted`: whether the transport aborted an envelope fetch since it was last reset. -// `onAbort`: called each time the transport aborts an envelope fetch. -export const lastSend: { aborted: boolean; onAbort?: () => void } = { aborted: false }; +// Whether the transport aborted an envelope fetch since it was last reset. +export const lastSend: { aborted: boolean } = { aborted: false }; diff --git a/dev-packages/cloudflare-integration-tests/suites/queue/index.ts b/dev-packages/cloudflare-integration-tests/suites/queue/index.ts index 44d20be6e939..eb47d5b981e6 100644 --- a/dev-packages/cloudflare-integration-tests/suites/queue/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/queue/index.ts @@ -10,6 +10,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1, + queueTracePropagationBindings: ['MY_QUEUE'], }), { async fetch(request, env) { @@ -41,6 +42,11 @@ export default Sentry.withSentry( return new Response('not found', { status: 404 }); }, async queue(batch: MessageBatch<{ trigger?: 'error'; payload?: string }>) { + Sentry.getActiveSpan()?.setAttribute( + 'test.queue.message_bodies', + JSON.stringify(batch.messages.map(message => message.body)), + ); + for (const message of batch.messages) { if (message.body.trigger === 'error') { throw new Error('Boom from queue handler'); diff --git a/dev-packages/cloudflare-integration-tests/suites/queue/test.ts b/dev-packages/cloudflare-integration-tests/suites/queue/test.ts index 9093e6624998..08432b7dfe45 100644 --- a/dev-packages/cloudflare-integration-tests/suites/queue/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/queue/test.ts @@ -1,4 +1,5 @@ -import type { Envelope } from '@sentry/core'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import type { Envelope, SerializedStreamedSpan } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../runner'; import { getSpansFromEnvelope } from '../../spanUtils'; @@ -30,11 +31,13 @@ it('captures errors thrown by the queue handler with the correct mechanism', asy it('emits a queue.publish span on env.MY_QUEUE.send and a queue.process segment span on the consumer', async ({ signal, }) => { + let publishSpan: SerializedStreamedSpan | undefined; + let processSpan: SerializedStreamedSpan | undefined; const runner = createRunner(__dirname) .unordered() .expect((envelope: Envelope) => { // The producer trace carries a `queue.publish` child span. - const publishSpan = getSpansFromEnvelope(envelope).find(span => span.name === 'send MY_QUEUE'); + publishSpan = getSpansFromEnvelope(envelope).find(span => span.name === 'send MY_QUEUE'); expect(publishSpan).toBeDefined(); expect(publishSpan?.attributes).toEqual( @@ -63,20 +66,35 @@ it('emits a queue.publish span on env.MY_QUEUE.send and a queue.process segment 'messaging.operation.name': { type: 'string', value: 'process' }, 'messaging.batch.message_count': { type: 'integer', value: 1 }, 'faas.trigger': { type: 'string', value: 'pubsub' }, + // The handler must not see the trace context the producer injected. + 'test.queue.message_bodies': { type: 'string', value: '[{"payload":"hello"}]' }, }), ); + processSpan = segmentSpan; }) .start(signal); await runner.makeRequest('post', '/enqueue/ok'); await runner.completed(); + + expect(processSpan?.trace_id).not.toBe(publishSpan?.trace_id); + expect(processSpan?.links).toEqual([ + { + trace_id: publishSpan?.trace_id, + span_id: publishSpan?.span_id, + sampled: true, + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'message_origin' } }, + }, + ]); }); it('emits a queue.publish span with batch attributes on env.MY_QUEUE.sendBatch', async ({ signal }) => { + let publishSpan: SerializedStreamedSpan | undefined; + let processSpan: SerializedStreamedSpan | undefined; const runner = createRunner(__dirname) .unordered() .expect((envelope: Envelope) => { - const publishSpan = getSpansFromEnvelope(envelope).find(span => span.name === 'send MY_QUEUE'); + publishSpan = getSpansFromEnvelope(envelope).find(span => span.name === 'send MY_QUEUE'); expect(publishSpan).toBeDefined(); expect(publishSpan?.attributes).toEqual( @@ -96,11 +114,26 @@ it('emits a queue.publish span with batch attributes on env.MY_QUEUE.sendBatch', expect(segmentSpan?.name).toBe('process test-queue'); expect(segmentSpan?.attributes['messaging.batch.message_count']).toEqual({ type: 'integer', value: 3 }); + expect(segmentSpan?.attributes['test.queue.message_bodies']).toEqual({ + type: 'string', + value: '[{"payload":"one"},{"payload":"two"},{"payload":"three"}]', + }); + processSpan = segmentSpan; }) .start(signal); await runner.makeRequest('post', '/enqueue/batch'); await runner.completed(); + + // All three messages come from one publish span, so the consumer links it once. + expect(processSpan?.links).toEqual([ + { + trace_id: publishSpan?.trace_id, + span_id: publishSpan?.span_id, + sampled: true, + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'message_origin' } }, + }, + ]); }); it('calls non-instrumented Queue methods like metrics() on the underlying binding', async ({ signal }) => { diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/streamed-response/test.ts b/dev-packages/cloudflare-integration-tests/suites/request-handler/streamed-response/test.ts index 9bc187460cbe..a45ca968b7c2 100644 --- a/dev-packages/cloudflare-integration-tests/suites/request-handler/streamed-response/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/streamed-response/test.ts @@ -30,6 +30,7 @@ it.for([ expect(getSpanOp(segmentSpan!)).toBe('http.server'); expect(renderSpan?.parent_span_id).toBe(segmentSpan?.span_id); - expect(segmentSpan!.end_timestamp).toBeGreaterThanOrEqual(renderSpan!.end_timestamp); + // Allow one millisecond for rounding differences in epoch timestamps. + expect(segmentSpan!.end_timestamp).toBeGreaterThanOrEqual(renderSpan!.end_timestamp - 0.001); }, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts index 8bf8f8acd720..625b77bc139e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts @@ -60,7 +60,11 @@ export default Sentry.withSentry( messages: [{ role: 'user', content: 'What is the weather in SF?' }], }); - return new Response(JSON.stringify({ success: true })); + const stream = await compiled.stream( + { messages: [{ role: 'user', content: 'Stream the weather in SF' }] }, + { encoding: 'text/event-stream' }, + ); + return new Response(stream as unknown as ReadableStream, { headers: { 'content-type': 'text/event-stream' } }); }, }, ); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts index 87008d27d870..4aa2a3ca4623 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts @@ -5,6 +5,7 @@ import { GEN_AI_OPERATION_NAME, GEN_AI_PIPELINE_NAME, GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, @@ -17,7 +18,7 @@ import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; // want to test that the instrumentation does not break in our // cloudflare SDK. -it('traces langgraph compile and invoke operations', async ({ signal }) => { +it('traces langgraph invoke and stream operations', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { @@ -26,10 +27,22 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => { expect(segmentSpan?.name).toBe('GET /'); const genAiSpans = spans.filter(span => getSpanOp(span)?.startsWith('gen_ai.')); - expect(genAiSpans).toHaveLength(1); - expect(genAiSpans.map(span => span.name).sort()).toEqual(['invoke_agent weather_assistant']); + expect(genAiSpans).toHaveLength(2); + expect(genAiSpans.map(span => span.name)).toEqual([ + 'invoke_agent weather_assistant', + 'invoke_agent weather_assistant', + ]); - const invokeAgentSpan = genAiSpans.find(span => span.name === 'invoke_agent weather_assistant'); + const streamAgentSpan = genAiSpans.find(span => span.attributes[GEN_AI_RESPONSE_STREAMING]); + expect(streamAgentSpan!.status).toBe('ok'); + expect(streamAgentSpan!.attributes[GEN_AI_RESPONSE_STREAMING]).toEqual({ type: 'boolean', value: true }); + expect(streamAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"Stream the weather in SF"}]', + }); + expect(streamAgentSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toEqual({ type: 'string', value: 'mock-model' }); + + const invokeAgentSpan = genAiSpans.find(span => !span.attributes[GEN_AI_RESPONSE_STREAMING]); expect(invokeAgentSpan).toBeDefined(); expect(invokeAgentSpan!.status).toBe('ok'); expect(invokeAgentSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({ diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/index.ts new file mode 100644 index 000000000000..4812bc2b1d9a --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/index.ts @@ -0,0 +1,30 @@ +import { getTunnelPath } from '@sentry/core'; +import { serveTunnelIntegration } from '@sentry/core/server'; +import { wrapRequestHandler } from '@sentry/cloudflare/request'; + +interface Env { + SENTRY_DSN: string; +} + +export default { + async fetch(request, env, ctx) { + return wrapRequestHandler( + { + options: { + dsn: env.SENTRY_DSN, + // The app answers the tunnel requests itself. + integrations: [serveTunnelIntegration({ intercept: false })], + }, + request, + context: ctx, + }, + () => { + if (new URL(request.url).pathname === '/tunnel-path') { + return new Response(getTunnelPath(env.SENTRY_DSN)); + } + + throw new Error('The app failed to answer a tunnel request'); + }, + ); + }, +} satisfies ExportedHandler; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/test.ts new file mode 100644 index 000000000000..2fdda3122c1f --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/test.ts @@ -0,0 +1,23 @@ +import type { Event } from '@sentry/core'; +import { expect, it } from 'vitest'; +import { createRunner } from '../../../runner'; + +it('captures errors the app throws while it answers tunnel requests, with their own request data', async ({ + signal, +}) => { + let tunnelPath: string | undefined; + const runner = createRunner(__dirname) + .expect(envelope => { + const event = envelope[1]?.[0]?.[1] as Event; + expect(event.exception?.values?.[0]?.value).toBe('The app failed to answer a tunnel request'); + // Without `AsyncLocalStorage`, the previous request's data would still be on the shared isolation scope. + expect(event.request?.method).toBe('POST'); + expect(new URL(event.request!.url!).pathname).toBe(tunnelPath); + }) + .start(signal); + + tunnelPath = await runner.makeRequest('get', '/tunnel-path'); + await runner.makeRequest('post', tunnelPath!, { data: 'envelope', expectError: true }); + + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/wrangler.jsonc new file mode 100644 index 000000000000..8da1d3b14f41 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "managed-tunnel-app-errors", + "compatibility_date": "2025-06-17", + "main": "index.ts", + // No `nodejs_compat`, so no `AsyncLocalStorage`: all requests share one isolation scope. +} diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/instrument.server.ts index fce513e4d092..9c7d270a4801 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/instrument.server.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/instrument.server.ts @@ -3,6 +3,5 @@ import { defineCloudflareOptions, serveTunnelIntegration } from '@sentry/cloudfl export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1, - // A meta-framework answers the tunnel requests, the Worker only drops their server spans. integrations: [serveTunnelIntegration()], })); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/test.ts index 98e9570be20a..c104d68a30cd 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel/test.ts @@ -10,7 +10,7 @@ function expectServerSpanAt(path: string) { }; } -it('drops the server span of managed tunnel requests', async ({ signal }) => { +it('answers managed tunnel requests before the app, without a server span', async ({ signal }) => { // Envelopes are expected in order, so a span of the tunnel request would arrive before the last one. const runner = createRunner(__dirname) .expect(expectServerSpanAt('/tunnel-path')) @@ -18,7 +18,8 @@ it('drops the server span of managed tunnel requests', async ({ signal }) => { .start(signal); const tunnelPath = await runner.makeRequest('get', '/tunnel-path'); - expect(await runner.makeRequest('post', tunnelPath!, { data: 'envelope' })).toBe('app'); + // The tunnel rejects the body as an envelope. The app would have answered with a 200. + await runner.makeRequest('post', tunnelPath!, { data: 'envelope', expectError: true }); await runner.makeRequest('get', '/after-tunnel'); await runner.completed(); diff --git a/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/index.ts b/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/index.ts new file mode 100644 index 000000000000..96d9669509e0 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/index.ts @@ -0,0 +1,32 @@ +import * as Sentry from '@sentry/cloudflare'; + +interface Env { + SENTRY_DSN: string; + ALLOWED_DSNS: string; + TUNNEL_PATH: string; +} + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.SENTRY_DSN, + integrations: [Sentry.serveTunnelIntegration({ allowedDsns: env.ALLOWED_DSNS.split(',') })], + }), + { + async fetch(request, env) { + const { pathname } = new URL(request.url); + if (pathname === '/health') { + return new Response('ok'); + } + if (pathname === '/done') { + Sentry.captureMessage('done'); + return new Response('done'); + } + // The app's own handler on the tunnel path, which the runtime answers before it. + if (pathname === env.TUNNEL_PATH) { + return new Response('app tunnel handler', { status: 418 }); + } + + return new Response('not found', { status: 404 }); + }, + } satisfies ExportedHandler, +); diff --git a/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/test.ts b/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/test.ts new file mode 100644 index 000000000000..a7a10f44c776 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/test.ts @@ -0,0 +1,51 @@ +import { getTunnelPath } from '@sentry/core'; +import type { FakeIngests } from '@sentry-internal/test-utils'; +import { EXPECTED_TUNNEL_OUTCOMES, runTunnelHardeningCases, startFakeIngests } from '@sentry-internal/test-utils'; +import { afterAll, beforeAll, expect, it } from 'vitest'; +import { createRunner } from '../../runner'; + +// `wrangler dev` stalls the next request after any worker answers before reading a large body, Sentry or not. +const WRANGLER_DEV_STALLS = ['70KB header line', 'binary garbage', 'rejected tunnel with a big unread body, then app']; + +let ingests: FakeIngests; + +beforeAll(async () => { + ingests = await startFakeIngests(); +}); + +afterAll(async () => { + await ingests.close(); +}); + +it('keeps the worker serving whatever is thrown at the tunnel, and reports nothing of its own', async ({ signal }) => { + const runner = createRunner(__dirname) + .withWranglerArgs( + '--var', + `ALLOWED_DSNS:${ingests.allDsns().join(',')}`, + '--var', + `TUNNEL_PATH:${getTunnelPath(ingests.dsn('ok'))}`, + ) + // The first event is the app's own, so the tunnel captured no error along the way. + .expect(envelope => { + expect(envelope[1][0]?.[1]).toMatchObject({ message: 'done' }); + }) + .start(signal); + await runner.makeRequest('get', '/health'); + + const results = await runTunnelHardeningCases({ + port: await runner.getPort(), + ingests, + getTunnelPath, + skip: WRANGLER_DEV_STALLS, + }); + + expect(results.filter(result => result.health !== '200')).toEqual([]); + expect(Object.fromEntries(results.map(result => [result.name, result.outcome]))).toMatchObject( + Object.fromEntries( + Object.entries(EXPECTED_TUNNEL_OUTCOMES).filter(([name]) => !WRANGLER_DEV_STALLS.includes(name)), + ), + ); + + await runner.makeRequest('get', '/done'); + await runner.completed(); +}, 180_000); diff --git a/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/wrangler.jsonc new file mode 100644 index 000000000000..d6be01281f0c --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/wrangler.jsonc @@ -0,0 +1,6 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", + "compatibility_flags": ["nodejs_compat"], +} diff --git a/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/index.ts b/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/index.ts index 442c8073cf92..d247b70ac483 100644 --- a/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/index.ts @@ -1,6 +1,7 @@ import * as Sentry from '@sentry/cloudflare'; import { WorkflowEntrypoint } from 'cloudflare:workers'; import type { WorkflowEvent, WorkflowStep } from 'cloudflare:workers'; +import { NonRetryableError } from 'cloudflare:workflows'; interface Env { SENTRY_DSN: string; @@ -11,6 +12,7 @@ interface WorkflowParams { failCount: number; captureManual?: boolean; captureManualTwice?: boolean; + nonRetryable?: boolean; } class StepContextTestWorkflowBase extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep): Promise { @@ -36,6 +38,10 @@ class StepContextTestWorkflowBase extends WorkflowEntrypoint 0) { remainingFailures--; throw new Error('Intentional failure for retry test'); @@ -66,10 +72,11 @@ export default Sentry.withSentry( const failCount = parseInt(url.searchParams.get('failCount') || '0', 10); const captureManual = url.searchParams.get('captureManual') === 'true'; const captureManualTwice = url.searchParams.get('captureManualTwice') === 'true'; + const nonRetryable = url.searchParams.get('nonRetryable') === 'true'; try { const instance = await env.STEP_CONTEXT_WORKFLOW.create({ - params: { failCount, captureManual, captureManualTwice }, + params: { failCount, captureManual, captureManualTwice, nonRetryable }, }); return new Response(JSON.stringify({ id: instance.id }), { headers: { 'Content-Type': 'application/json' } }); diff --git a/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/test.ts b/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/test.ts index c71ad223140a..a4c8af52620b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/test.ts @@ -59,6 +59,33 @@ it('With step context, only one error is captured on final retry attempt', async await runner.completed(); }); +it('Captures a NonRetryableError thrown before the final attempt', async ({ signal }) => { + const runner = createRunner(__dirname) + .expect((envelope: Envelope): void => { + const [, items] = envelope; + const [itemHeader, itemBody] = items[0] as [{ type: string }, Record]; + + expect(itemHeader.type).toBe('event'); + + const exception = itemBody.exception as { values?: Array<{ type?: string; value?: string }> }; + expect(exception?.values?.[0]).toMatchObject({ + type: 'NonRetryableError', + value: 'Intentional non-retryable failure', + }); + }) + .expect(flushMarkerMatcher) + .start(signal); + + const trigger = await runner.makeRequest('get', '/trigger-workflow?nonRetryable=true'); + expect(trigger?.id).toBeDefined(); + + const status = await waitForWorkflowStatus(runner.makeRequest.bind(runner), trigger!.id); + expect(status?.status?.status).toBe('errored'); + + await runner.makeRequest('get', '/flush-marker'); + await runner.completed(); +}); + it('No error event when step eventually succeeds within retry limit', async ({ signal }) => { const runner = createRunner(__dirname).expect(flushMarkerMatcher).start(signal); diff --git a/dev-packages/deno-integration-tests/package.json b/dev-packages/deno-integration-tests/package.json index ad5f92931816..e7a6c681f4f2 100644 --- a/dev-packages/deno-integration-tests/package.json +++ b/dev-packages/deno-integration-tests/package.json @@ -17,11 +17,11 @@ }, "devDependencies": { "@sentry-internal/node-integration-tests": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/deno": "11.11.0", "mysql": "^2.18.1", "pg": "^8.23.1", - "vitest": "^4.1.11" + "vitest": "^4.1.12" } } diff --git a/dev-packages/deno-integration-tests/suites/deno-serve-managed-tunnel/test.ts b/dev-packages/deno-integration-tests/suites/deno-serve-managed-tunnel/test.ts index 4e67982d7e47..b25896253c77 100644 --- a/dev-packages/deno-integration-tests/suites/deno-serve-managed-tunnel/test.ts +++ b/dev-packages/deno-integration-tests/suites/deno-serve-managed-tunnel/test.ts @@ -7,14 +7,13 @@ import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index. const DSN = 'https://username@domain/123'; -Deno.test('Deno.serve drops the server span of managed tunnel requests', async () => { +Deno.test('Deno.serve answers managed tunnel requests before the app, without a server span', async () => { resetGlobals(); const sink = spanSink(); init({ dsn: DSN, tracesSampleRate: 1, - // A meta-framework answers the tunnel requests, Deno only drops their server spans. integrations: [serveTunnelIntegration()], transport: sink.transport, }); @@ -33,7 +32,9 @@ Deno.test('Deno.serve drops the server span of managed tunnel requests', async ( 'segment span for /after-tunnel', ); - await (await fetch(`${baseUrl}${getTunnelPath(DSN)}`, { method: 'POST', body: 'envelope' })).text(); + const tunnelResponse = await fetch(`${baseUrl}${getTunnelPath(DSN)}`, { method: 'POST', body: 'envelope' }); + // Answered by the tunnel, which rejects the body as an envelope, not by the app. + assertEquals(await tunnelResponse.text(), 'Invalid envelope'); await (await fetch(`${baseUrl}/after-tunnel`)).text(); await flush(); diff --git a/dev-packages/e2e-tests/lib/getTestMatrix.mjs b/dev-packages/e2e-tests/lib/getTestMatrix.mjs index 447ff5ad6f01..0f52cd017a2e 100644 --- a/dev-packages/e2e-tests/lib/getTestMatrix.mjs +++ b/dev-packages/e2e-tests/lib/getTestMatrix.mjs @@ -37,6 +37,7 @@ function run() { head: { type: 'string' }, variant: { type: 'string', default: 'required' }, shards: { type: 'string' }, + 'separate-aws-layer': { type: 'boolean', default: false }, }, }); @@ -62,12 +63,18 @@ function run() { ? getAffectedTestApplications(testApplications, { base, head }) : testApplications; - const includes = []; + let includes = []; includedTestApplications.forEach(testApp => { addIncludesForTestApp(testApp, includes, { variant }); }); + if (values['separate-aws-layer']) { + const layerIncludes = includes.filter(row => row['test-application'] === 'aws-serverless-layer'); + includes = includes.filter(row => row['test-application'] !== 'aws-serverless-layer'); + process.stdout.write(`matrix-aws-layer=${JSON.stringify({ include: layerIncludes })}\n`); + } + if (!shards) { // We print this to the output, so the GHA can use it for the matrix // eslint-disable-next-line no-console diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 8e257cf133fa..e62ae3902510 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -10,16 +10,13 @@ "test:run": "node run.mjs", "test:prepare": "node prepare.mjs", "test:validate": "node validate-packed-tarball-setup.mjs", - "clean": "rimraf tmp node_modules packed && pnpm run clean:test-applications && pnpm run clean:pnpm", + "clean": "node ../../scripts/rm.mjs tmp node_modules packed && pnpm run clean:test-applications && pnpm run clean:pnpm", "ci:build-matrix": "node ./lib/getTestMatrix.mjs", "ci:build-matrix-optional": "node ./lib/getTestMatrix.mjs --variant=optional", "ci:build-matrix-canary": "node ./lib/getTestMatrix.mjs --variant=canary", "ci:copy-to-temp": "node ./ciCopyToTemp.mjs", "ci:pnpm-overrides": "node ./ciPnpmOverrides.mjs", - "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular}", + "clean:test-applications": "node scripts/clean-test-applications.mjs", "clean:pnpm": "pnpm store prune" - }, - "devDependencies": { - "rimraf": "^6.1.3" } } diff --git a/dev-packages/e2e-tests/scripts/clean-test-applications.mjs b/dev-packages/e2e-tests/scripts/clean-test-applications.mjs new file mode 100644 index 000000000000..31039850a9e1 --- /dev/null +++ b/dev-packages/e2e-tests/scripts/clean-test-applications.mjs @@ -0,0 +1,8 @@ +import { globSync, rmSync } from 'node:fs'; + +const pattern = + 'test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular}'; + +for (const path of globSync(pattern)) { + rmSync(path, { recursive: true, force: true }); +} diff --git a/dev-packages/e2e-tests/test-applications/angular-17/package.json b/dev-packages/e2e-tests/test-applications/angular-17/package.json index 3dfb8de63f42..f435511287bd 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/package.json @@ -15,13 +15,10 @@ }, "private": true, "dependencies": { - "@angular/animations": "^17.1.0", "@angular/common": "^17.1.0", "@angular/compiler": "^17.1.0", "@angular/core": "^17.1.0", - "@angular/forms": "^17.1.0", "@angular/platform-browser": "^17.1.0", - "@angular/platform-browser-dynamic": "^17.1.0", "@angular/router": "^17.1.0", "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz", "rxjs": "~7.8.0", @@ -31,18 +28,10 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@angular-devkit/build-angular": "^17.1.1", "@angular/cli": "^17.1.1", "@angular/compiler-cli": "^17.1.0", - "@types/jasmine": "~5.1.0", "http-server": "^14.1.1", - "jasmine-core": "~5.1.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.3.2" } } diff --git a/dev-packages/e2e-tests/test-applications/angular-18/package.json b/dev-packages/e2e-tests/test-applications/angular-18/package.json index a18c5e59adea..78898ae27c65 100644 --- a/dev-packages/e2e-tests/test-applications/angular-18/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-18/package.json @@ -15,13 +15,10 @@ }, "private": true, "dependencies": { - "@angular/animations": "^18.0.0", "@angular/common": "^18.0.0", "@angular/compiler": "^18.0.0", "@angular/core": "^18.0.0", - "@angular/forms": "^18.0.0", "@angular/platform-browser": "^18.0.0", - "@angular/platform-browser-dynamic": "^18.0.0", "@angular/router": "^18.0.0", "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz", "rxjs": "~7.8.0", @@ -31,18 +28,10 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@angular-devkit/build-angular": "^18.0.0", "@angular/cli": "^18.0.0", "@angular/compiler-cli": "^18.0.0", - "@types/jasmine": "~5.1.0", "http-server": "^14.1.1", - "jasmine-core": "~5.1.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.4.5" } } diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json index 1c0a004af2fe..8fdc1cd3f236 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -18,13 +18,10 @@ }, "private": true, "dependencies": { - "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", "@angular/compiler": "^19.0.0", "@angular/core": "^19.0.0", - "@angular/forms": "^19.0.0", "@angular/platform-browser": "^19.0.0", - "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz", "rxjs": "~7.8.0", @@ -36,17 +33,9 @@ "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", "@playwright/test": "1.64.1", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "@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.6.2" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json index 3f9606b689de..da0c624541db 100644 --- a/dev-packages/e2e-tests/test-applications/angular-20/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json @@ -15,13 +15,10 @@ }, "private": true, "dependencies": { - "@angular/animations": "^20.0.0", "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", - "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", - "@angular/platform-browser-dynamic": "^20.0.0", "@angular/router": "^20.0.0", "@sentry/angular": "file:../../packed/sentry-angular-packed.tgz", "rxjs": "~7.8.0", @@ -34,15 +31,7 @@ "@angular/compiler-cli": "^20.0.0", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "@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" } } diff --git a/dev-packages/e2e-tests/test-applications/angular-21/package.json b/dev-packages/e2e-tests/test-applications/angular-21/package.json index ce311f876192..251c574d8fd3 100644 --- a/dev-packages/e2e-tests/test-applications/angular-21/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-21/package.json @@ -35,15 +35,7 @@ "@angular/compiler-cli": "^21.0.0", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "@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": "~6.0.0" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/angular-22/package.json b/dev-packages/e2e-tests/test-applications/angular-22/package.json index 7384f9368932..07907a701d92 100644 --- a/dev-packages/e2e-tests/test-applications/angular-22/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-22/package.json @@ -35,15 +35,7 @@ "@angular/compiler-cli": "^22.0.0", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "@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": "~6.0.0" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/IsolationScope/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/IsolationScope/index.js new file mode 100644 index 000000000000..9e18e6ac0deb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/IsolationScope/index.js @@ -0,0 +1,9 @@ +const Sentry = require('@sentry/aws-serverless'); + +exports.handler = async event => { + if (event.tag) { + Sentry.setTag('invocation_tag', event.tag); + } + + Sentry.captureException(new Error(event.errorMessage)); +}; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts index f3ae0d58ceb0..5d3eb3701a2e 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts @@ -394,6 +394,33 @@ test.describe('Lambda layer', () => { expect(transactionEvent.contexts?.trace?.parent_span_id).toEqual(parentSpanId); }); + test('does not leak isolation scope data between invocations on a warm container', async ({ lambdaClient }) => { + const firstErrorEventPromise = waitForError('aws-serverless-layer', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'first invocation error'; + }); + const secondErrorEventPromise = waitForError('aws-serverless-layer', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'second invocation error'; + }); + + await lambdaClient.send( + new InvokeCommand({ + FunctionName: 'LayerIsolationScope', + Payload: JSON.stringify({ tag: 'first-invocation', errorMessage: 'first invocation error' }), + }), + ); + await lambdaClient.send( + new InvokeCommand({ + FunctionName: 'LayerIsolationScope', + Payload: JSON.stringify({ errorMessage: 'second invocation error' }), + }), + ); + + const [firstErrorEvent, secondErrorEvent] = await Promise.all([firstErrorEventPromise, secondErrorEventPromise]); + + expect(firstErrorEvent.tags?.invocation_tag).toBe('first-invocation'); + expect(secondErrorEvent.tags?.invocation_tag).toBeUndefined(); + }); + test('extension tunnel validates DSN allowlist and rejects invalid envelopes', async ({ lambdaClient }) => { const matchingMarker = `extension-tunnel-matching-${Date.now()}`; const matchingRequestPromise = waitForRequest('aws-serverless-layer', requestData => { diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/index.mjs new file mode 100644 index 000000000000..914944147e85 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/index.mjs @@ -0,0 +1 @@ +export const handler = async () => ({ statusCode: 200, body: 'app' }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/instrument.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/instrument.mjs new file mode 100644 index 000000000000..b513b159600c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/instrument.mjs @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + debug: true, + integrations: [Sentry.serveTunnelIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/index.js new file mode 100644 index 000000000000..2dabb97238dc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/index.js @@ -0,0 +1,3 @@ +exports.handler = (_event, _context, callback) => { + callback(null, { statusCode: 200, body: 'app' }); +}; diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/instrument.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/instrument.mjs new file mode 100644 index 000000000000..b513b159600c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/instrument.mjs @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + debug: true, + integrations: [Sentry.serveTunnelIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/index.mjs new file mode 100644 index 000000000000..914944147e85 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/index.mjs @@ -0,0 +1 @@ +export const handler = async () => ({ statusCode: 200, body: 'app' }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/instrument.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/instrument.mjs new file mode 100644 index 000000000000..ce00ff2f3659 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/instrument.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + debug: true, + // Served on an API Gateway stage named `prod`, where the browser posts to `/prod/`. + integrations: [Sentry.serveTunnelIntegration({ basePath: '/prod' })], +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/index.mjs new file mode 100644 index 000000000000..680bc8969deb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/index.mjs @@ -0,0 +1,4 @@ +export const handler = awslambda.streamifyResponse(async (_event, responseStream) => { + responseStream.write('app'); + responseStream.end(); +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/instrument.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/instrument.mjs new file mode 100644 index 000000000000..b513b159600c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/instrument.mjs @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + debug: true, + integrations: [Sentry.serveTunnelIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/index.mjs new file mode 100644 index 000000000000..680bc8969deb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/index.mjs @@ -0,0 +1,4 @@ +export const handler = awslambda.streamifyResponse(async (_event, responseStream) => { + responseStream.write('app'); + responseStream.end(); +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/instrument.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/instrument.mjs new file mode 100644 index 000000000000..bf77ac2fa2a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/instrument.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + // The test's ingest on this port answers 200 and then breaks off mid-body. + dsn: process.env.SENTRY_DSN.replace(':3031/', ':3033/'), + debug: true, + integrations: [Sentry.serveTunnelIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/index.mjs new file mode 100644 index 000000000000..680bc8969deb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/index.mjs @@ -0,0 +1,4 @@ +export const handler = awslambda.streamifyResponse(async (_event, responseStream) => { + responseStream.write('app'); + responseStream.end(); +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/instrument.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/instrument.mjs new file mode 100644 index 000000000000..64648c0caac6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/instrument.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/aws-serverless'; + +Sentry.init({ + // Nothing listens on this port, so forwarding a tunnel request fails after the tunnel claimed it. + dsn: process.env.SENTRY_DSN.replace(':3031/', ':3032/'), + debug: true, + integrations: [Sentry.serveTunnelIntegration()], +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts index 6b0b980ae5c5..6de922abc3b8 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts @@ -123,7 +123,10 @@ export class LocalLambdaStack extends Stack { SENTRY_DSN: dsn, SENTRY_TRACES_SAMPLE_RATE: 1.0, SENTRY_DEBUG: true, - NODE_OPTIONS: `--import=@sentry/aws-serverless/awslambda-auto`, + // A function with its own `instrument.mjs` initializes the SDK itself, e.g. to add integrations. + NODE_OPTIONS: fs.existsSync(path.join(lambdaPath, 'instrument.mjs')) + ? '--import=/var/task/instrument.mjs' + : '--import=@sentry/aws-serverless/awslambda-auto', }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/tunnel.test.ts new file mode 100644 index 000000000000..7f96818d70f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/tunnel.test.ts @@ -0,0 +1,261 @@ +import { InvokeCommand } from '@aws-sdk/client-lambda'; +import type { LambdaClient } from '@aws-sdk/client-lambda'; +import { waitForError } from '@sentry-internal/test-utils'; +import { createServer } from 'node:http'; +import { getTunnelPath } from '@sentry/aws-serverless'; +import { getHostIp } from '../src/stack'; +import { expect, test } from './lambda-fixtures'; + +/** + * The headers of Sentry's answer the Node tunnel passes on, see `FORWARDED_RESPONSE_HEADERS` in `@sentry/core`. + */ +const FORWARDED_RESPONSE_HEADERS = ['content-type', 'retry-after', 'x-sentry-rate-limits']; + +interface InvokeResult { + functionError?: string; + payload: string; +} + +async function invoke(lambdaClient: LambdaClient, functionName: string, event: unknown): Promise { + const result = await lambdaClient.send( + new InvokeCommand({ FunctionName: functionName, Payload: JSON.stringify(event) }), + ); + + return { functionError: result.FunctionError, payload: Buffer.from(result.Payload ?? []).toString('utf8') }; +} + +async function getDsn(port = 3031): Promise { + return `http://public@${await getHostIp()}:${port}/1337`; +} + +function makeEnvelope(dsn: string, marker: string): string { + const eventId = crypto.randomUUID().replace(/-/g, ''); + + return [ + JSON.stringify({ dsn, event_id: eventId, sent_at: new Date().toISOString() }), + JSON.stringify({ type: 'event' }), + JSON.stringify({ event_id: eventId, message: marker, level: 'info' }), + ].join('\n'); +} + +type EventShape = 'apiGatewayV2' | 'apiGatewayV1' | 'alb'; + +/** + * The event API Gateway (HTTP API or function URL, and REST), or an ALB, hands the handler for a browser POSTing an + * envelope to the tunnel. + */ +function makeHttpEvent(shape: EventShape, path: string, envelope: string, base64 = false): Record { + const body = base64 ? Buffer.from(envelope).toString('base64') : envelope; + const contentType = 'application/x-sentry-envelope'; + + if (shape === 'apiGatewayV2') { + return { + version: '2.0', + rawPath: path, + rawQueryString: '', + headers: { 'content-type': contentType }, + requestContext: { http: { method: 'POST' } }, + body, + isBase64Encoded: base64, + }; + } + + if (shape === 'apiGatewayV1') { + return { httpMethod: 'POST', path, headers: { 'content-type': contentType }, body, isBase64Encoded: base64 }; + } + + return { + httpMethod: 'POST', + path, + multiValueHeaders: { 'content-type': [contentType] }, + requestContext: { elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:us-east-1:012345678912:targetgroup/t' } }, + body, + isBase64Encoded: base64, + }; +} + +test.describe('managed tunnel', () => { + for (const functionName of ['NpmTunnelAsync', 'NpmTunnelCallback']) { + test(`${functionName} answers tunnel requests before the handler`, async ({ lambdaClient }) => { + const dsn = await getDsn(); + const marker = `${functionName}-${crypto.randomUUID()}`; + const forwarded = waitForError('aws-serverless', event => event.message === marker); + + const result = await invoke( + lambdaClient, + functionName, + makeHttpEvent('apiGatewayV2', getTunnelPath(dsn)!, makeEnvelope(dsn, marker)), + ); + + expect(result.functionError).toBeUndefined(); + expect(JSON.parse(result.payload)).toMatchObject({ statusCode: 200, isBase64Encoded: false }); + expect(JSON.parse(result.payload).body).not.toBe('app'); + await forwarded; + }); + + test(`${functionName} passes other events to the handler`, async ({ lambdaClient }) => { + const result = await invoke( + lambdaClient, + functionName, + makeHttpEvent('apiGatewayV2', '/other', 'not an envelope'), + ); + + expect(JSON.parse(result.payload)).toEqual({ statusCode: 200, body: 'app' }); + }); + } + + // An ALB event with multi-value headers needs multi-value headers back. + for (const [shape, base64, headersKey] of [ + ['apiGatewayV1', false, 'headers'], + ['alb', false, 'multiValueHeaders'], + ['apiGatewayV2', true, 'headers'], + ] as const) { + test(`reads ${base64 ? 'a base64 body from ' : ''}an ${shape} event`, async ({ lambdaClient }) => { + const dsn = await getDsn(); + const marker = `${shape}-${base64}-${crypto.randomUUID()}`; + const forwarded = waitForError('aws-serverless', event => event.message === marker); + + const result = await invoke( + lambdaClient, + 'NpmTunnelAsync', + makeHttpEvent(shape, getTunnelPath(dsn)!, makeEnvelope(dsn, marker), base64), + ); + + const response = JSON.parse(result.payload); + expect(response.statusCode).toBe(200); + expect(response).toHaveProperty(headersKey); + // Only the headers the browser SDK reads, like the Node answer, not the ones about the connection to Sentry. + const headerNames = Object.keys(response[headersKey]); + expect(headerNames.filter(name => !FORWARDED_RESPONSE_HEADERS.includes(name))).toEqual([]); + await forwarded; + }); + } + + for (const functionName of ['NpmTunnelStreaming']) { + test(`${functionName} streams the tunnel answer before the handler`, async ({ lambdaClient }) => { + const dsn = await getDsn(); + const marker = `${functionName}-${crypto.randomUUID()}`; + const forwarded = waitForError('aws-serverless', event => event.message === marker); + + const result = await invoke( + lambdaClient, + functionName, + makeHttpEvent('apiGatewayV2', getTunnelPath(dsn)!, makeEnvelope(dsn, marker)), + ); + + expect(result.functionError).toBeUndefined(); + expect(result.payload).not.toContain('app'); + await forwarded; + }); + } + + test('ends the stream with the error answer when forwarding a tunnel request fails', async ({ lambdaClient }) => { + // The function's DSN points at a port nothing listens on. + const dsn = await getDsn(3032); + const startedAt = Date.now(); + + const result = await invoke( + lambdaClient, + 'NpmTunnelStreamingUnreachable', + makeHttpEvent('apiGatewayV2', getTunnelPath(dsn)!, makeEnvelope(dsn, 'unreachable')), + ); + + // An unended stream would hold the invocation until the 10s function timeout. + expect(result.functionError).toBeUndefined(); + expect(Date.now() - startedAt).toBeLessThan(8_000); + expect(result.payload).toBe('Failed to forward envelope to Sentry'); + }); + + test('ends the stream with an error answer when the answer from Sentry breaks off', async ({ lambdaClient }) => { + // Answers 200, then drops the connection before the body is complete. + const ingest = createServer((req, res) => { + req.resume(); + req.on('end', () => { + res.writeHead(200, { 'content-type': 'application/json', 'content-length': '100' }); + res.write('{"id":'); + setTimeout(() => res.socket?.destroy(), 50); + }); + }).listen(3033, '0.0.0.0'); + + try { + const dsn = await getDsn(3033); + const startedAt = Date.now(); + + const result = await invoke( + lambdaClient, + 'NpmTunnelStreamingBrokenAnswer', + makeHttpEvent('apiGatewayV2', getTunnelPath(dsn)!, makeEnvelope(dsn, 'broken-answer')), + ); + + // An unended stream would hold the invocation until the 10s function timeout. + expect(result.functionError).toBeUndefined(); + expect(Date.now() - startedAt).toBeLessThan(8_000); + expect(result.payload).not.toContain('app'); + } finally { + ingest.close(); + } + }); + + // The shapes a real API Gateway sends on a stage named `prod`: a REST API leaves the stage out of `path` but keeps it + // in `requestContext.path`, an HTTP API keeps it in `rawPath`. + for (const [api, makeStageEvent] of [ + [ + 'REST API', + (tunnelPath: string, envelope: string) => ({ + httpMethod: 'POST', + path: tunnelPath, + headers: { 'content-type': 'application/x-sentry-envelope' }, + requestContext: { path: `/prod${tunnelPath}`, stage: 'prod' }, + body: envelope, + isBase64Encoded: false, + }), + ], + [ + 'HTTP API', + (tunnelPath: string, envelope: string) => ({ + version: '2.0', + rawPath: `/prod${tunnelPath}`, + rawQueryString: '', + headers: { 'content-type': 'application/x-sentry-envelope' }, + requestContext: { http: { method: 'POST' }, stage: 'prod' }, + body: envelope, + isBase64Encoded: false, + }), + ], + ] as const) { + test(`answers tunnel requests on a named ${api} stage with the stage as \`basePath\``, async ({ lambdaClient }) => { + const dsn = await getDsn(); + const marker = `stage-${api}-${crypto.randomUUID()}`; + const forwarded = waitForError('aws-serverless', event => event.message === marker); + + const result = await invoke( + lambdaClient, + 'NpmTunnelStagePath', + makeStageEvent(getTunnelPath(dsn)!, makeEnvelope(dsn, marker)), + ); + + expect(JSON.parse(result.payload).body).not.toBe('app'); + await forwarded; + }); + } + + // Where the browser posted to the stage-stripped `path`, like a custom domain mapped to the stage. + test('answers a REST API tunnel request when only its stage-stripped path matches', async ({ lambdaClient }) => { + const dsn = await getDsn(); + const marker = `stage-stripped-${crypto.randomUUID()}`; + const forwarded = waitForError('aws-serverless', event => event.message === marker); + const tunnelPath = getTunnelPath(dsn)!; + + const result = await invoke(lambdaClient, 'NpmTunnelAsync', { + httpMethod: 'POST', + path: tunnelPath, + headers: { 'content-type': 'application/x-sentry-envelope' }, + requestContext: { path: `/prod${tunnelPath}`, stage: 'prod' }, + body: makeEnvelope(dsn, marker), + isBase64Encoded: false, + }); + + expect(JSON.parse(result.payload).body).not.toBe('app'); + await forwarded; + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts b/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts index 2a157ff659c6..18a7ec9498f7 100644 --- a/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts +++ b/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts @@ -36,7 +36,7 @@ test('reports a hit on a genuine back/forward-cache restore', async ({ page }) = // Renderer-initiated history navigation restores the page from bfcache. // (Playwright's CDP `goBack` bypasses bfcache, so we trigger it from within the page.) - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); // Fail fast with a clear signal if the environment did not actually restore from bfcache. await page.waitForFunction(() => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, { @@ -71,7 +71,7 @@ test('reports a miss with notRestoredReasons when an unload listener blocks bfca await page.waitForTimeout(500); // The unload listener makes the page ineligible, so this back navigation is a fresh reload (a miss). - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction( () => (performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined)?.type === @@ -121,7 +121,7 @@ test('reports a miss for an open WebSocket on Chrome < 149 (a hit from 149 on)', await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); const outcome = await outcomePromise; expect(outcome.value).toBe(1); @@ -152,7 +152,7 @@ test('reports a miss with an idbversionchangeevent reason when a connection bloc await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); const miss = await missPromise; expect(miss.value).toBe(1); @@ -180,7 +180,7 @@ test('reports a miss with a response-cache-control-no-store reason when a CCNS p await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); const miss = await missPromise; expect(miss.value).toBe(1); @@ -201,7 +201,7 @@ test('restores a page whose embedded iframe is bfcache-eligible', async ({ page await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction(() => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, { timeout: 5000, }); @@ -239,7 +239,7 @@ test('reports a child-frame reason when an ineligible iframe blocks the top page await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); const miss = await missPromise; expect(miss.value).toBe(1); @@ -268,7 +268,7 @@ test('a hit carries the parameterized route that was on the scope before the fre await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction(() => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, { timeout: 5000, }); @@ -295,7 +295,7 @@ test('a hit carries the route resolved by a registered route provider', async ({ await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction(() => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, { timeout: 5000, }); @@ -316,7 +316,7 @@ test('a hit falls back to the raw pathname when no route is on the scope', async await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction(() => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, { timeout: 5000, }); @@ -343,7 +343,7 @@ test('a miss reload does not carry a pre-freeze route', async ({ page }) => { await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2'); await page.waitForTimeout(500); - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction( () => (performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined)?.type === @@ -374,7 +374,7 @@ test.describe('the navigation span for a restore', () => { await page.waitForTimeout(500); // Renderer-initiated, because Playwright's CDP `goBack` bypasses bfcache. - await page.evaluate(() => history.back()); + await page.goBack({ waitUntil: 'commit' }); await page.waitForFunction( () => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json index 45e8c7dcda56..1855fcbfaf82 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/package.json @@ -33,9 +33,7 @@ "@types/react-dom": "^19.2.3", "@types/ws": "^8.18.1", "@babel/core": "^8.0.1", - "@babel/plugin-proposal-decorators": "^8.0.2", "@vitejs/plugin-react": "^6.0.4", - "globals": "^17.8.0", "typescript": "~6.0.3", "vite": "^8.1.5", "wrangler": "^4.114.0", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/agent-socket.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/agent-socket.ts index 96437eb47738..f9e6fd96cd20 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/agent-socket.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/agent-socket.ts @@ -1,8 +1,8 @@ import WebSocket from 'ws'; -type AgentReply = { type?: string; id?: string; done?: boolean }; +type AgentReply = { type?: string; id?: string; done?: boolean; success?: boolean; error?: string; result?: unknown }; -/** Sends a single frame over a WS to the given agent instance and resolves once `isDone` matches a reply. */ +/** Sends a single frame over a WS to the given agent instance and resolves with the first reply that `isDone` matches. */ function driveAgentSocket( baseURL: string, binding: string, @@ -10,10 +10,10 @@ function driveAgentSocket( frame: unknown, isDone: (reply: AgentReply) => boolean, timeoutLabel: string, -): Promise { +): Promise { const wsUrl = `${baseURL.replace(/^http/, 'ws')}/agents/${binding}/${instance}`; - return new Promise((resolveSocket, rejectSocket) => { + return new Promise((resolveSocket, rejectSocket) => { const socket = new WebSocket(wsUrl); const timeout = setTimeout(() => { socket.close(); @@ -30,7 +30,7 @@ function driveAgentSocket( if (isDone(parsed)) { clearTimeout(timeout); socket.close(); - resolveSocket(); + resolveSocket(parsed); } } catch { // Ignore non-JSON / unrelated frames. @@ -45,7 +45,7 @@ function driveAgentSocket( } /** Opens a chat WebSocket to `/agents//`, sends one `cf_agent_use_chat_request` frame. */ -export function sendChatMessage( +export async function sendChatMessage( baseURL: string, options: { binding: string; instance: string; prompt: string }, ): Promise { @@ -61,7 +61,7 @@ export function sendChatMessage( }, }; - return driveAgentSocket( + await driveAgentSocket( baseURL, options.binding, options.instance, @@ -71,20 +71,29 @@ export function sendChatMessage( ); } -/** Opens a WebSocket to `/agents//`, sends one RPC frame, resolves on the reply. */ -export function callRpc( +/** + * Opens a WebSocket to `/agents//`, sends one RPC frame, and resolves with the + * method's result. Rejects with the agent's error message when the RPC fails. + */ +export async function callRpc( baseURL: string, options: { binding: string; instance: string; method: string; args: unknown[] }, -): Promise { +): Promise { const id = `rpc-${options.method}`; const frame = { type: 'rpc', id, method: options.method, args: options.args }; - return driveAgentSocket( + const reply = await driveAgentSocket( baseURL, options.binding, options.instance, frame, - reply => reply.type === 'rpc' && reply.id === id && !!reply.done, + reply => reply.type === 'rpc' && reply.id === id && (!!reply.done || reply.success === false), `RPC reply to "${options.method}"`, ); + + if (!reply.success) { + throw new Error(`RPC "${options.method}" failed: ${reply.error}`); + } + + return reply.result; } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/constructor-failure.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/constructor-failure.test.ts new file mode 100644 index 000000000000..fce0a9f208fc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/constructor-failure.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; +import { callRpc } from './agent-socket'; + +const AGENT_INSTANCE = 'recovering-instance'; + +// `MyRecoveringAgent` throws on its first construction in the isolate, after `agents` has wrapped its +// `@callable()` methods. A retry of this test runs in the same isolate and cannot fail that first +// construction again, so it fails on the status check instead of passing without the failure. +test('keeps @callable() methods callable after the first Agent construction throws', async ({ baseURL, request }) => { + const failedConstruction = await request.get(`${baseURL}/agents/my-recovering-agent/${AGENT_INSTANCE}`); + + expect(failedConstruction.status()).toBe(500); + + await expect( + callRpc(baseURL!, { binding: 'my-recovering-agent', instance: AGENT_INSTANCE, method: 'greet', args: ['World'] }), + ).resolves.toBe('Hello, World!'); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts index c68101bcc04a..35d263fce8a7 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker-configuration.d.ts @@ -6,11 +6,12 @@ interface __BaseEnv_Env { MyAgent: DurableObjectNamespace; MyChatAgent: DurableObjectNamespace; MyManualChatAgent: DurableObjectNamespace; + MyRecoveringAgent: DurableObjectNamespace; } declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import('./worker/index'); - durableNamespaces: 'MyAgent' | 'MyChatAgent' | 'MyManualChatAgent'; + durableNamespaces: 'MyAgent' | 'MyChatAgent' | 'MyManualChatAgent' | 'MyRecoveringAgent'; } interface Env extends __BaseEnv_Env {} } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts index f230af50eba0..b92ac6e41c2a 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/worker/index.ts @@ -100,9 +100,31 @@ class MyManualChatAgentBase extends AIChatAgent { } } +// `agents` wraps the `@callable()` methods of a class during its first construction in the isolate. +// Throwing after `super()` fails that construction after the wrapping, like a storage error during +// Agent initialization (for example a Durable Object storage quota limit) does. +let failNextRecoveringAgentConstruction = true; + +class MyRecoveringAgentBase extends Agent { + public constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + + if (failNextRecoveringAgentConstruction) { + failNextRecoveringAgentConstruction = false; + throw new Error('Simulated storage failure during the first construction'); + } + } + + @callable() + async greet(name: string): Promise { + return `Hello, ${name}!`; + } +} + export const MyAgent = Sentry.instrumentAgentWithSentry(sentryOptions, MyBaseAgent); export const MyChatAgent = Sentry.instrumentAgentWithSentry(sentryOptions, MyChatAgentBase); export const MyManualChatAgent = Sentry.instrumentAgentWithSentry(sentryOptions, MyManualChatAgentBase); +export const MyRecoveringAgent = Sentry.instrumentAgentWithSentry(sentryOptions, MyRecoveringAgentBase); export default Sentry.withSentry(sentryOptions, { async fetch(request, env): Promise { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc index 9c4cc5f9a16e..baef0601035f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/cloudflare-agent/wrangler.jsonc @@ -27,10 +27,13 @@ { "name": "MyAgent", "class_name": "MyAgent" }, { "name": "MyChatAgent", "class_name": "MyChatAgent" }, { "name": "MyManualChatAgent", "class_name": "MyManualChatAgent" }, + { "name": "MyRecoveringAgent", "class_name": "MyRecoveringAgent" }, ], }, - "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent", "MyChatAgent", "MyManualChatAgent"] }], + "migrations": [ + { "tag": "v1", "new_sqlite_classes": ["MyAgent", "MyChatAgent", "MyManualChatAgent", "MyRecoveringAgent"] }, + ], "version_metadata": { "binding": "CF_VERSION_METADATA", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/package.json index ccdb6208ee1f..86cb0a8cc650 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-autoinstrument/package.json @@ -19,7 +19,6 @@ }, "devDependencies": { "@babel/core": "^8.0.1", - "@babel/plugin-proposal-decorators": "^8.0.2", "@cloudflare/vite-plugin": "^1.47.0", "@cloudflare/workers-types": "^5.20260727.1", "@playwright/test": "1.64.1", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json index 48963fd70fa8..1536539c7659 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/package.json @@ -21,7 +21,6 @@ "@cloudflare/workers-types": "^4.20240725.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "typescript": "^5.5.2", - "wrangler": "^4.61.0", - "ws": "^8.18.3" + "wrangler": "^4.61.0" } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mastra/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mastra/package.json index 15ba4efc715c..689cdaf4a0a6 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mastra/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mastra/package.json @@ -30,8 +30,7 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "typescript": "^5.5.2", "vite": "7.3.5", - "wrangler": "^4.86.0", - "ws": "^8.18.3" + "wrangler": "^4.86.0" }, "sentryTest": { "optional": true diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json index 6b2cb1ea3043..9c878cf5dcb0 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-mcp/package.json @@ -24,7 +24,6 @@ "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "typescript": "^5.5.2", - "wrangler": "^4.61.0", - "ws": "^8.18.3" + "wrangler": "^4.61.0" } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/package.json index b7de7ca0eea5..bb4aa66b5d45 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/package.json @@ -24,7 +24,6 @@ "@types/node": "^24.12.4", "typescript": "^5.5.2", "vite": "7.3.2", - "wrangler": "^4.61.0", - "ws": "^8.18.3" + "wrangler": "^4.61.0" } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json index 5989cce49a5f..b3d9c70eb8a9 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/package.json @@ -8,21 +8,23 @@ "preview": "vite preview --port 38789", "typecheck": "tsc --noEmit", "test:build": "pnpm install && pnpm build", - "test:build-latest": "pnpm install && pnpm add agents@latest @earendil-works/pi-durable@latest @earendil-works/pi-ai@latest && pnpm build", + "test:build-latest": "pnpm install && pnpm add agents@latest @earendil-works/pi-durable@latest @earendil-works/pi-ai@latest @earendil-works/chord@latest && pnpm build", "test:assert": "playwright test" }, "dependencies": { + "@earendil-works/chord": "1.2.0", "@earendil-works/pi-ai": "1.2.0", "@earendil-works/pi-durable": "1.2.0", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", - "agents": "0.26.0" + "agents": "0.26.0", + "typebox": "1.3.27" }, "devDependencies": { "@cloudflare/vite-plugin": "1.57.2", "@cloudflare/workers-types": "^4.20260426.0", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "typescript": "^5.5.2", "vite": "7.3.5", "wrangler": "^4.136.2" diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts index 93e1236190cf..0a4d577f35f7 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/assistant.ts @@ -1,4 +1,4 @@ -import { Type } from '@earendil-works/pi-ai'; +import { Type } from 'typebox'; import { createModels } from '@earendil-works/pi-ai/models'; import { openrouterProvider } from '@earendil-works/pi-ai/providers/openrouter'; import { createRegistry, defineExtension, defineTool, Harness, section } from '@earendil-works/pi-durable'; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/eager.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/eager.ts new file mode 100644 index 000000000000..0fac1197709f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/eager.ts @@ -0,0 +1,75 @@ +import { createModels } from '@earendil-works/pi-ai/models'; +import { openrouterProvider } from '@earendil-works/pi-ai/providers/openrouter'; +import { createRegistry, Harness, ProviderDoc } from '@earendil-works/pi-durable'; +import * as Sentry from '@sentry/cloudflare'; +import { openPiSessionStore } from 'agents/harness/pi'; +import { DurableObject } from 'cloudflare:workers'; + +// chord's `BACKGROUND_CONTEXT`, which pi-durable does not re-export. +const BACKGROUND_CONTEXT = Object.freeze({ + abortSignal: undefined, + value: () => undefined, + toString: () => '[Context background]', +}) as unknown as Parameters[2]; + +/** + * A plain Durable Object that opens pi-durable's Harness in its constructor, before any of its + * handlers runs. + */ +class EagerAssistantBase extends DurableObject { + private harness!: Harness; + + public constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + void ctx.blockConcurrencyWhile(async () => { + const models = createModels({ + authContext: { + env: async name => (name === 'OPENROUTER_API_KEY' ? env.E2E_OPENROUTER_API_KEY : undefined), + fileExists: async () => false, + }, + }); + models.setProvider(openrouterProvider()); + const storage = await openPiSessionStore(ctx.storage); + this.harness = await Harness.open(storage, { models, registry: createRegistry() }, BACKGROUND_CONTEXT); + this.harness.resume(); + }); + } + + public async fetch(request: Request): Promise { + const conversationId = Number(new URL(request.url).searchParams.get('conversation')) as Parameters< + Harness['conversation'] + >[0]; + if (!conversationId) { + const conversation = await this.harness.createConversation( + { + ownership: { kind: 'ownerless' }, + agent: { model: { provider: 'openrouter', modelId: 'anthropic/claude-haiku-4.5' } }, + }, + BACKGROUND_CONTEXT, + ); + // The SDK reports the provider session id as `gen_ai.conversation.id`. + const provider = await this.harness.snapshot(ProviderDoc, conversation.id, BACKGROUND_CONTEXT); + return Response.json({ id: conversation.id, sessionId: provider?.sessionId }); + } + + const { message } = await request.json<{ message: string }>(); + const conversation = await this.harness.conversation(conversationId, BACKGROUND_CONTEXT); + const submission = await conversation!.submit({ type: 'input', content: message }, BACKGROUND_CONTEXT); + const settled = await submission.wait(BACKGROUND_CONTEXT); + return Response.json(settled); + } +} + +export const EagerAssistant = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', + tunnel: 'http://localhost:3031/', + tracesSampleRate: 1.0, + }), + EagerAssistantBase, +); + +export default { + fetch: () => new Response('Not found', { status: 404 }), +} satisfies ExportedHandler; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/env.d.ts index a088008cd966..69f40d9af2e4 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/env.d.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/env.d.ts @@ -3,6 +3,7 @@ declare namespace Cloudflare { E2E_TEST_DSN: string; E2E_OPENROUTER_API_KEY: string; Assistant: DurableObjectNamespace; + EagerAssistant: DurableObjectNamespace; } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/index.ts index 756f31007b6b..73f61acc6c94 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/index.ts @@ -4,6 +4,10 @@ export { Assistant } from './assistant'; export default { async fetch(request, env): Promise { + const eager = new URL(request.url).pathname.match(/^\/eager\/([^/]+)$/); + if (eager) { + return env.EagerAssistant.get(env.EagerAssistant.idFromName(eager[1]!)).fetch(request); + } return (await routeAgentRequest(request, env)) ?? new Response('Not found', { status: 404 }); }, } satisfies ExportedHandler; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/pi-durable.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/pi-durable.test.ts index 0c8f04674b12..f27454078339 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/pi-durable.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/pi-durable.test.ts @@ -14,7 +14,7 @@ import { } from '@sentry/conventions/attributes'; import { DB_QUERY, GEN_AI_CHAT, GEN_AI_INVOKE_AGENT, HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op'; import { collectStreamedSpans, getSpanOp, waitForError } from '@sentry-internal/test-utils'; -import { newAgentId, prompt, submitPrompt, waitForOperation } from './utils'; +import { createEagerConversation, newAgentId, prompt, promptEager, submitPrompt, waitForOperation } from './utils'; const APP = 'cloudflare-pi-durable'; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; @@ -149,3 +149,30 @@ test('resumes a run in a new trace after the Durable Object resets during a tool // tool. A run that never reset would start with that request. expect(chats.every(chat => chat.start_timestamp >= tool.start_timestamp)).toBe(true); }); + +test('traces a Durable Object that opens the Harness in its constructor', async ({ baseURL }) => { + // The object runs in its own Worker, so its isolate has no earlier handler that initialized the SDK. + const objectId = newAgentId('eager'); + const conversation = await createEagerConversation(baseURL!, objectId); + const spansPromise = collectStreamedSpans(APP, spansOfTrace => + spansOfTrace.some( + span => + span.is_segment && + getSpanOp(span) === GEN_AI_INVOKE_AGENT && + span.attributes[GEN_AI_CONVERSATION_ID]?.value === conversation.sessionId, + ), + ); + + const answer = await promptEager(baseURL!, objectId, conversation.id, 'Reply with the word DONE.'); + expect(answer.status, answer.reason).toBe('done'); + + const spans = await spansPromise; + const agent = spans.find(span => span.is_segment)!; + const chats = spans.filter(span => getSpanOp(span) === GEN_AI_CHAT); + + expect(agent.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.pi_durable'); + expect(chats.length).toBeGreaterThan(0); + for (const chat of chats) { + expect(chat.parent_span_id).toBe(agent.span_id); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/utils.ts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/utils.ts index 0e23ca86301e..e7ae89eca3d0 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/utils.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/tests/utils.ts @@ -36,3 +36,28 @@ export async function waitForOperation(baseURL: string, agentId: string, operati expect(res.status).toBe(200); return (await res.json()) as Operation; } + +/** Create a conversation in a new `EagerAssistant`, which opens its Harness in the constructor. */ +export async function createEagerConversation( + baseURL: string, + objectId: string, +): Promise<{ id: number; sessionId: string }> { + const res = await fetch(`${baseURL}/eager/${objectId}`, { method: 'POST' }); + expect(res.status).toBe(200); + return (await res.json()) as { id: number; sessionId: string }; +} + +export async function promptEager( + baseURL: string, + objectId: string, + conversationId: number, + message: string, +): Promise { + const res = await fetch(`${baseURL}/eager/${objectId}?conversation=${conversationId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message }), + }); + expect(res.status).toBe(200); + return (await res.json()) as Operation; +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/vite.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/vite.config.mts index 005f4448f6cb..dd9a498e9851 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/vite.config.mts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/vite.config.mts @@ -3,5 +3,5 @@ import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [cloudflare(), sentryCloudflareVitePlugin()], + plugins: [cloudflare({ auxiliaryWorkers: [{ configPath: './wrangler.eager.jsonc' }] }), sentryCloudflareVitePlugin()], }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.eager.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.eager.jsonc new file mode 100644 index 000000000000..223d28f1e176 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.eager.jsonc @@ -0,0 +1,12 @@ +// A second Worker, so `EagerAssistant` runs in an isolate where no Worker handler has called +// `Sentry.init()` before the Durable Object is constructed. +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "cloudflare-pi-durable-eager", + "main": "src/eager.ts", + "compatibility_date": "2026-09-15", + "compatibility_flags": ["nodejs_compat"], + "secrets": { "required": ["E2E_TEST_DSN", "E2E_OPENROUTER_API_KEY"] }, + "durable_objects": { "bindings": [{ "name": "EagerAssistant", "class_name": "EagerAssistant" }] }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["EagerAssistant"] }], +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.jsonc new file mode 100644 index 000000000000..bacd8d6e9f8e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "cloudflare-pi-durable", + "main": "src/index.ts", + "compatibility_date": "2026-09-15", + "compatibility_flags": ["nodejs_compat"], + // `vite preview` takes no `--var` flag, so the secrets are declared here and read from + // the process environment the Playwright web server inherits. + "secrets": { "required": ["E2E_TEST_DSN", "E2E_OPENROUTER_API_KEY"] }, + "durable_objects": { + "bindings": [ + { "name": "Assistant", "class_name": "Assistant" }, + // Lives in the `cloudflare-pi-durable-eager` Worker, see `wrangler.eager.jsonc`. + { "name": "EagerAssistant", "class_name": "EagerAssistant", "script_name": "cloudflare-pi-durable-eager" }, + ], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Assistant"] }], +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.toml deleted file mode 100644 index 1b2207bd862f..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.toml +++ /dev/null @@ -1,18 +0,0 @@ -#:schema node_modules/wrangler/config-schema.json -name = "cloudflare-pi-durable" -main = "src/index.ts" -compatibility_date = "2026-09-15" -compatibility_flags = ["nodejs_compat"] - -# `vite preview` takes no `--var` flag, so the secrets are declared here and read from -# the process environment the Playwright web server inherits. -[secrets] -required = ["E2E_TEST_DSN", "E2E_OPENROUTER_API_KEY"] - -[[durable_objects.bindings]] -name = "Assistant" -class_name = "Assistant" - -[[migrations]] -tag = "v1" -new_sqlite_classes = ["Assistant"] diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-think/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-think/package.json index 584491363792..04ec772e1836 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-think/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-think/package.json @@ -30,8 +30,7 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "typescript": "^5.5.2", "vite": "7.3.5", - "wrangler": "^4.136.2", - "ws": "^8.18.3" + "wrangler": "^4.136.2" }, "sentryTest": { "optional": true, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json index 8f75bd48b26b..929b0ce99e46 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json @@ -23,7 +23,6 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "typescript": "^5.5.2", "vite": "7.3.5", - "wrangler": "^4.86.0", - "ws": "^8.18.3" + "wrangler": "^4.86.0" } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json index 78012b914707..0e01d1cec1c9 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json @@ -19,7 +19,7 @@ "@cloudflare/workers-types": "^5.20260727.1", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@types/node": "^26.1.2", "sentry": "~0.44.1", "typescript": "~6.0.3", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts index ea4e136d5536..50726f22d6e5 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts @@ -7,7 +7,7 @@ import { GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, } from '@sentry/conventions/attributes'; -import { GEN_AI_CHAT, GEN_AI_DECIDE, HTTP_SERVER } from '@sentry/conventions/op'; +import { GEN_AI_CHAT, GEN_AI_DECIDE, GEN_AI_EXTRACT, HTTP_SERVER } from '@sentry/conventions/op'; import { EVENT_POLLING_OPTIONS, fetchSpanAttributes, @@ -152,9 +152,8 @@ test('Sends a Workers AI gen_ai.extract span for a Clairic call to Sentry', asyn console.log(`Polling for gen_ai.extract span: sentry trace view ${traceTarget(traceId)}`); let spanId: string | undefined; - // TODO: Use `GEN_AI_EXTRACT` from `@sentry/conventions/op` once getsentry/sentry-conventions#728 is released await expect - .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.extract')?.event_id), EVENT_POLLING_OPTIONS) + .poll(() => (spanId = findSpanInTrace(traceId, GEN_AI_EXTRACT)?.event_id), EVENT_POLLING_OPTIONS) .toBeDefined(); await expect diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index 3d9a8c55afca..0f8efc6bfdf5 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -4,7 +4,6 @@ "scripts": { "build": "remix vite:build && pnpm typecheck", "dev": "node ./server.mjs", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "start": "cross-env NODE_ENV=production node ./server.mjs", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", @@ -33,15 +32,8 @@ "@types/morgan": "^1.9.9", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.7.4", "cross-env": "^7.0.3", "esbuild": "0.28.1", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", "typescript": "^5.1.6", "vite": "^5.4.11", "vite-tsconfig-paths": "^4.2.1" diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json index f15a38048bef..6fa1fabfd91f 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/package.json @@ -5,7 +5,6 @@ "scripts": { "build": "remix vite:build && pnpm typecheck", "dev": "node ./server.mjs", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "start": "cross-env NODE_ENV=production node ./server.mjs", "typecheck": "tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", @@ -24,8 +23,7 @@ "isbot": "^4.1.0", "morgan": "^1.10.0", "react": "^18.2.0", - "react-dom": "^18.2.0", - "source-map-support": "^0.5.21" + "react-dom": "^18.2.0" }, "devDependencies": { "@playwright/test": "1.64.1", @@ -36,16 +34,6 @@ "@types/morgan": "^1.9.4", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", - "@types/source-map-support": "^0.5.6", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "chokidar": "^3.5.3", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "tsx": "4.7.2", "typescript": "^5.1.6", "vite": "^5.4.11", "vite-tsconfig-paths": "^4.2.1" diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json index 9023c5b8487a..2fedda1aaa64 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-non-vite/package.json @@ -24,10 +24,8 @@ "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@remix-run/dev": "2.17.4", - "@remix-run/eslint-config": "2.17.4", "@types/react": "^18.0.35", "@types/react-dom": "^18.0.11", - "eslint": "^8.38.0", "typescript": "^5.0.4" } } diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/package.json index efa2c4cf4761..78af52f8e58a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2-static/package.json @@ -27,12 +27,9 @@ "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@remix-run/dev": "2.17.4", - "@remix-run/eslint-config": "2.17.4", "@types/mysql": "^2.15.26", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.34", - "@types/prop-types": "15.7.7", - "eslint": "^8.38.0", "typescript": "^5.1.6", "vite": "^5.4.11", "vite-tsconfig-paths": "^4.2.1" diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json index efa2c4cf4761..78af52f8e58a 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-v2/package.json @@ -27,12 +27,9 @@ "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@remix-run/dev": "2.17.4", - "@remix-run/eslint-config": "2.17.4", "@types/mysql": "^2.15.26", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.34", - "@types/prop-types": "15.7.7", - "eslint": "^8.38.0", "typescript": "^5.1.6", "vite": "^5.4.11", "vite-tsconfig-paths": "^4.2.1" diff --git a/dev-packages/e2e-tests/test-applications/elysia-node/package.json b/dev-packages/e2e-tests/test-applications/elysia-node/package.json index f5b18d32baed..a140f4867551 100644 --- a/dev-packages/e2e-tests/test-applications/elysia-node/package.json +++ b/dev-packages/e2e-tests/test-applications/elysia-node/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@sentry/elysia": "file:../../packed/sentry-elysia-packed.tgz", - "elysia": "latest", + "elysia": "^1.4.0", "@elysiajs/node": "^1.4.5" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/gen-ai-libraries/package.json b/dev-packages/e2e-tests/test-applications/gen-ai-libraries/package.json index 3d0ff3e11d38..a8c03f02a9ab 100644 --- a/dev-packages/e2e-tests/test-applications/gen-ai-libraries/package.json +++ b/dev-packages/e2e-tests/test-applications/gen-ai-libraries/package.json @@ -33,7 +33,7 @@ "@cloudflare/workers-types": "^4.20260426.0", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@types/express": "^4.17.21", "@types/node": "^18.19.1", "tsx": "4.21.0", diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json index 53a6fc1b33b6..da3dd4ad3029 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json @@ -16,6 +16,7 @@ "dependencies": { "@sentry/bun": "latest || *", "@sentry/cloudflare": "latest || *", + "@sentry/core": "latest || *", "@sentry/deno": "latest || *", "@sentry/hono": "latest || *", "@sentry/node": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts index ac6fea1320cf..473df4b9e9ad 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/route-groups/test-route-patterns.ts @@ -1,3 +1,4 @@ +import { captureMessage } from '@sentry/core'; import { Hono } from 'hono'; const routePatterns = new Hono(); @@ -30,4 +31,24 @@ METHODS.forEach(method => { routePatterns.on(method, '/on', c => c.text(`${method} on response`)); }); +// The catch-all below also matches. An event from inside this handler must still get this route. +routePatterns.get('/capture/:id', c => { + captureMessage('Message from a route handler'); + return c.text('captured'); +}); +routePatterns.get('/capture/*', c => c.text('capture catch-all')); + +// Passes `draft` on to the catch-all below, which then names the request. +routePatterns.get('/posts/:id', async (c, next) => { + if (c.req.param('id') === 'draft') { + await next(); + return; + } + return c.text('post'); +}); +routePatterns.get('/posts/*', c => { + captureMessage('Message from a route handler after next()'); + return c.text('fallback'); +}); + export { routePatterns }; diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts index d760f12e6451..6c2a04ac1040 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/middleware.test.ts @@ -1,5 +1,11 @@ +import { randomBytes } from 'node:crypto'; import { expect, test } from '@playwright/test'; -import { waitForError, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; +import { + waitForError, + getSpanOp, + collectStreamedSpans, + collectStreamedSpansUntilSegment, +} from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const SCENARIOS = [ @@ -49,7 +55,7 @@ for (const { name, prefix } of SCENARIOS) { expect(middlewareSpan?.status).not.toBe('error'); const durationMs = (middlewareSpan!.end_timestamp - middlewareSpan!.start_timestamp) * 1000; - expect(durationMs).toBeGreaterThanOrEqual(49); + expect(durationMs).toBeGreaterThanOrEqual(45); }); test('creates a span for anonymous middleware', async ({ baseURL }) => { @@ -138,11 +144,11 @@ for (const { name, prefix } of SCENARIOS) { expect(middlewareSpans[0]?.parent_span_id).toBe(middlewareSpans[1]?.parent_span_id); - // middlewareA has a 50ms delay, middlewareB has a 60ms delay + // middlewareA has a 50ms delay, middlewareB has a 60ms delay; allow 5ms of timer variance. const aDurationMs = (middlewareSpans[0].end_timestamp - middlewareSpans[0]?.start_timestamp) * 1000; const bDurationMs = (middlewareSpans[1].end_timestamp - middlewareSpans[1]?.start_timestamp) * 1000; - expect(aDurationMs).toBeGreaterThanOrEqual(49); - expect(bDurationMs).toBeGreaterThanOrEqual(59); + expect(aDurationMs).toBeGreaterThanOrEqual(45); + expect(bDurationMs).toBeGreaterThanOrEqual(55); }); test('captures error thrown in middleware', async ({ baseURL }) => { @@ -170,18 +176,20 @@ for (const { name, prefix } of SCENARIOS) { }); test('sets error status on middleware span when middleware throws', async ({ baseURL }) => { - const segmentPromise = collectStreamedSpansUntilSegment( - APP_NAME, - segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`), + // Other tests request the same route and only wait for the error event, so their segment can + // still arrive while this test listens. Pin the trace to tell this request's spans apart. + const traceId = randomBytes(16).toString('hex'); + const segmentPromise = collectStreamedSpans(APP_NAME, spansOfTrace => + spansOfTrace.some(span => span.is_segment && span.trace_id === traceId), ); - await fetch(`${baseURL}${prefix}/error`); + await fetch(`${baseURL}${prefix}/error`, { + headers: { 'sentry-trace': `${traceId}-${randomBytes(8).toString('hex')}-1` }, + }); const segmentSpans = await segmentPromise; - const segment = segmentSpans.find( - segment => - segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`), - )!; + const segment = segmentSpans.find(span => span.is_segment)!; + expect(getSpanOp(segment)).toBe('http.server'); expect(segment.name).toBe(`GET ${prefix}/error`); const spans = segmentSpans.filter( diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts index bfebb9f5b6ab..d1251fad340e 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tests/route-patterns.test.ts @@ -1,5 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; +import { + waitForError, + waitForStreamedSpan, + getSpanOp, + collectStreamedSpansUntilSegment, +} from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const PREFIX = '/test-routes'; @@ -176,3 +181,23 @@ test('async handler sends span', async ({ baseURL }) => { const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toEqual([]); }); + +test('names an event captured in a route handler after that route', async ({ baseURL }) => { + const eventPromise = waitForError(APP_NAME, event => event.message === 'Message from a route handler'); + + const response = await fetch(`${baseURL}${PREFIX}/capture/42`); + expect(response.status).toBe(200); + + const event = await eventPromise; + expect(event.transaction).toBe(`GET ${PREFIX}/capture/:id`); +}); + +test('names an event from the route handler that runs after next() after that route', async ({ baseURL }) => { + const eventPromise = waitForError(APP_NAME, event => event.message === 'Message from a route handler after next()'); + + const response = await fetch(`${baseURL}${PREFIX}/posts/draft`); + expect(response.status).toBe(200); + + const event = await eventPromise; + expect(event.transaction).toBe(`GET ${PREFIX}/posts/*`); +}); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json index baf473e3471e..14e62429c2f3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json @@ -22,6 +22,7 @@ "dependencies": { "@sentry/bun": "latest || *", "@sentry/cloudflare": "latest || *", + "@sentry/core": "latest || *", "@sentry/deno": "latest || *", "@sentry/node": "latest || *", "@hono/node-server": "^2.0.5", diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-route-patterns.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-route-patterns.ts index ac6fea1320cf..473df4b9e9ad 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-route-patterns.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-route-patterns.ts @@ -1,3 +1,4 @@ +import { captureMessage } from '@sentry/core'; import { Hono } from 'hono'; const routePatterns = new Hono(); @@ -30,4 +31,24 @@ METHODS.forEach(method => { routePatterns.on(method, '/on', c => c.text(`${method} on response`)); }); +// The catch-all below also matches. An event from inside this handler must still get this route. +routePatterns.get('/capture/:id', c => { + captureMessage('Message from a route handler'); + return c.text('captured'); +}); +routePatterns.get('/capture/*', c => c.text('capture catch-all')); + +// Passes `draft` on to the catch-all below, which then names the request. +routePatterns.get('/posts/:id', async (c, next) => { + if (c.req.param('id') === 'draft') { + await next(); + return; + } + return c.text('post'); +}); +routePatterns.get('/posts/*', c => { + captureMessage('Message from a route handler after next()'); + return c.text('fallback'); +}); + export { routePatterns }; diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts index d760f12e6451..6c2a04ac1040 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts @@ -1,5 +1,11 @@ +import { randomBytes } from 'node:crypto'; import { expect, test } from '@playwright/test'; -import { waitForError, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; +import { + waitForError, + getSpanOp, + collectStreamedSpans, + collectStreamedSpansUntilSegment, +} from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const SCENARIOS = [ @@ -49,7 +55,7 @@ for (const { name, prefix } of SCENARIOS) { expect(middlewareSpan?.status).not.toBe('error'); const durationMs = (middlewareSpan!.end_timestamp - middlewareSpan!.start_timestamp) * 1000; - expect(durationMs).toBeGreaterThanOrEqual(49); + expect(durationMs).toBeGreaterThanOrEqual(45); }); test('creates a span for anonymous middleware', async ({ baseURL }) => { @@ -138,11 +144,11 @@ for (const { name, prefix } of SCENARIOS) { expect(middlewareSpans[0]?.parent_span_id).toBe(middlewareSpans[1]?.parent_span_id); - // middlewareA has a 50ms delay, middlewareB has a 60ms delay + // middlewareA has a 50ms delay, middlewareB has a 60ms delay; allow 5ms of timer variance. const aDurationMs = (middlewareSpans[0].end_timestamp - middlewareSpans[0]?.start_timestamp) * 1000; const bDurationMs = (middlewareSpans[1].end_timestamp - middlewareSpans[1]?.start_timestamp) * 1000; - expect(aDurationMs).toBeGreaterThanOrEqual(49); - expect(bDurationMs).toBeGreaterThanOrEqual(59); + expect(aDurationMs).toBeGreaterThanOrEqual(45); + expect(bDurationMs).toBeGreaterThanOrEqual(55); }); test('captures error thrown in middleware', async ({ baseURL }) => { @@ -170,18 +176,20 @@ for (const { name, prefix } of SCENARIOS) { }); test('sets error status on middleware span when middleware throws', async ({ baseURL }) => { - const segmentPromise = collectStreamedSpansUntilSegment( - APP_NAME, - segment => getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`), + // Other tests request the same route and only wait for the error event, so their segment can + // still arrive while this test listens. Pin the trace to tell this request's spans apart. + const traceId = randomBytes(16).toString('hex'); + const segmentPromise = collectStreamedSpans(APP_NAME, spansOfTrace => + spansOfTrace.some(span => span.is_segment && span.trace_id === traceId), ); - await fetch(`${baseURL}${prefix}/error`); + await fetch(`${baseURL}${prefix}/error`, { + headers: { 'sentry-trace': `${traceId}-${randomBytes(8).toString('hex')}-1` }, + }); const segmentSpans = await segmentPromise; - const segment = segmentSpans.find( - segment => - segment.is_segment && getSpanOp(segment) === 'http.server' && !!segment.name?.includes(`${prefix}/error`), - )!; + const segment = segmentSpans.find(span => span.is_segment)!; + expect(getSpanOp(segment)).toBe('http.server'); expect(segment.name).toBe(`GET ${prefix}/error`); const spans = segmentSpans.filter( diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts index bfebb9f5b6ab..d1251fad340e 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/route-patterns.test.ts @@ -1,5 +1,10 @@ import { expect, test } from '@playwright/test'; -import { waitForStreamedSpan, getSpanOp, collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; +import { + waitForError, + waitForStreamedSpan, + getSpanOp, + collectStreamedSpansUntilSegment, +} from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; const PREFIX = '/test-routes'; @@ -176,3 +181,23 @@ test('async handler sends span', async ({ baseURL }) => { const middlewareSpans = spans.filter(s => getSpanOp(s) === 'middleware'); expect(middlewareSpans).toEqual([]); }); + +test('names an event captured in a route handler after that route', async ({ baseURL }) => { + const eventPromise = waitForError(APP_NAME, event => event.message === 'Message from a route handler'); + + const response = await fetch(`${baseURL}${PREFIX}/capture/42`); + expect(response.status).toBe(200); + + const event = await eventPromise; + expect(event.transaction).toBe(`GET ${PREFIX}/capture/:id`); +}); + +test('names an event from the route handler that runs after next() after that route', async ({ baseURL }) => { + const eventPromise = waitForError(APP_NAME, event => event.message === 'Message from a route handler after next()'); + + const response = await fetch(`${baseURL}${PREFIX}/posts/draft`); + expect(response.status).toBe(200); + + const event = await eventPromise; + expect(event.transaction).toBe(`GET ${PREFIX}/posts/*`); +}); diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index db70cefd152d..89a5c0f55909 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -15,15 +15,11 @@ "dependencies": { "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "@sentry/vite-plugin": "^5.4.1", "@shopify/hydrogen": "2026.4.7", "graphql": "^16.10.0", - "graphql-tag": "^2.12.6", - "isbot": "^5.1.22", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "7.16.0", - "react-router-dom": "7.16.0" + "react-router": "7.16.0" }, "devDependencies": { "@graphql-codegen/cli": "5.0.2", diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11-static/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11-static/package.json index 36a8ae821650..134a6f4b4d0d 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11-static/package.json @@ -28,17 +28,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", - "@nestjs/testing": "^11.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json index 97386efde9bf..02b2920e6bc3 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -28,17 +28,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", - "@nestjs/testing": "^11.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-12/package.json b/dev-packages/e2e-tests/test-applications/nestjs-12/package.json index db3903bfd091..62908d591ad8 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-12/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-12/package.json @@ -12,7 +12,7 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test": "playwright test", "test:build": "pnpm install && pnpm build", - "test:build-latest": "pnpm install && pnpm add @nestjs/common@latest @nestjs/core@latest @nestjs/platform-express@latest @nestjs/microservices@latest && pnpm add -D @nestjs/cli@latest @nestjs/testing@latest && pnpm build", + "test:build-latest": "pnpm install && pnpm add @nestjs/common@latest @nestjs/core@latest @nestjs/platform-express@latest @nestjs/microservices@latest && pnpm add -D @nestjs/cli@latest && pnpm build", "test:assert": "pnpm test" }, "dependencies": { @@ -30,17 +30,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^12.0.0", "@nestjs/schematics": "^12.0.0", - "@nestjs/testing": "^12.0.0", "@types/express": "^5.0.0", "@types/node": "^22.0.0", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.9.0" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json index c1b77a1b000b..21e568dd913a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-8/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-8/package.json @@ -28,17 +28,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json index e20dc9ccb320..f8efb728068e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic-with-graphql/package.json @@ -30,17 +30,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json index 76340f92e80a..3f3f1256e788 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/package.json @@ -29,17 +29,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json index 74adc5bda089..2996bf149835 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/package.json @@ -27,17 +27,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json index 170b04e67123..c4ead54a7bc5 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json @@ -29,16 +29,7 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json index d6febbcfbbd5..dae85f5e3906 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-graphql/package.json @@ -30,17 +30,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-webpack/package.json b/dev-packages/e2e-tests/test-applications/nestjs-webpack/package.json index 3dd2c2c44dad..57a779a5264a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-webpack/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-webpack/package.json @@ -27,7 +27,6 @@ "@types/express": "^4.17.17", "@types/node": "^18.19.1", "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json index 967045d49e4e..5b930992674a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/package.json @@ -26,17 +26,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json index 27fed5b9aa10..3ecde4e04d1a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/package.json @@ -26,17 +26,8 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/node": "^18.19.1", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json index 5529013fa11e..78951ab5b9b4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json @@ -33,11 +33,6 @@ "@types/node": "^22.15.21", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", - "eslint": "^8.57.0", - "eslint-config-next": "^15.5.9", - "postcss": "^8.4.39", "tailwindcss": "^3.4.3", "typescript": "^5.8.2" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-adapter/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-adapter/package.json index f26999360e44..85ca40e4ef3d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-adapter/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-adapter/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index 6174845c517d..4292a42c3843 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json @@ -28,17 +28,14 @@ "@opennextjs/cloudflare": "^1.20.12", "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "import-in-the-middle": "^1", "next": "^16", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^7", - "zod": "^3.22.4" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.27.0", + "@sentry/conventions": "0.32.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-standalone/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-standalone/package.json index 4ef0a0350d4b..e3da25f0d04c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-standalone/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-standalone/package.json @@ -19,12 +19,9 @@ }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "import-in-the-middle": "^2", "next": "16.3.3", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^8" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-static/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-static/package.json index 062c709ae23d..c384f9e4f557 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-static/package.json @@ -19,14 +19,12 @@ "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@vercel/queue": "^0.1.3", "ai": "^3.0.0", - "import-in-the-middle": "^2", "ioredis": "5.10.1", "next": "16.2.11", "openai": "5.18.1", "pg": "^8.13.1", "react": "19.1.0", "react-dom": "19.1.0", - "require-in-the-middle": "^8", "zod": "^3.22.4" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json index c6ee4a2dca46..6da961d2bf6c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json @@ -25,17 +25,14 @@ "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "import-in-the-middle": "^1", "next": "^16", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^7", - "zod": "^3.22.4" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.27.0", + "@sentry/conventions": "0.32.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json index 1dcd01c56f8e..521e135d1fc3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json @@ -23,11 +23,9 @@ "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@vercel/queue": "^0.1.3", "ai": "^3.0.0", - "import-in-the-middle": "^2", "next": "16.2.11", "react": "19.1.0", "react-dom": "19.1.0", - "require-in-the-middle": "^8", "zod": "^3.22.4" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json index 6e0d8bb66455..95bc74ff7675 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json @@ -13,12 +13,9 @@ }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "import-in-the-middle": "^2", "next": "16.2.11", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^8" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json index 2b01d1438956..20fd5a21697f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash/package.json @@ -14,12 +14,9 @@ }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", - "import-in-the-middle": "^2", "next": "16.2.11", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^8" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", 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 index cf85a2193e40..ca42dc5e99a1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json @@ -30,18 +30,14 @@ "@opennextjs/cloudflare": "^1.14.9", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", - "ai": "^3.0.0", - "import-in-the-middle": "^1", "next": "16.3.8", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^7", - "zod": "^3.22.4" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", 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 2da1fec6e7cb..aa1116924f00 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json @@ -36,14 +36,12 @@ "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@vercel/queue": "^0.1.3", "ai": "^3.0.0", - "import-in-the-middle": "^2", "ioredis": "5.10.1", "next": "16.4.2", "openai": "5.18.1", "pg": "^8.13.1", "react": "19.1.0", "react-dom": "19.1.0", - "require-in-the-middle": "^8", "sentry-metadata-test-dependency": "file:../../test-applications/nextjs-16/fixtures/metadata-dependency", "zod": "^3.22.4" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json index fa5eef6fd840..c6ae990845b6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-orpc/package.json @@ -21,21 +21,14 @@ "@orpc/client": "latest", "next": "14.2.35", "react": "18.3.1", - "react-dom": "18.3.1", - "server-only": "^0.0.1" + "react-dom": "18.3.1" }, "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@types/eslint": "^8.56.10", "@types/node": "^18.19.1", "@types/react": "18.3.1", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", - "eslint": "^8.57.0", - "eslint-config-next": "^14.2.4", - "postcss": "^8.4.39", "typescript": "^5.5.3", "zod": "^3.24.2" }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json b/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json index 57dbccddf186..64c289e9439a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-otlp/package.json @@ -26,11 +26,9 @@ "@opentelemetry/sdk-trace-node": "^2.9.0", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", - "import-in-the-middle": "^2", "next": "16.3.0", "react": "19.1.0", - "react-dom": "19.1.0", - "require-in-the-middle": "^8" + "react-dom": "19.1.0" }, "devDependencies": { "@playwright/test": "1.64.1", diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/package.json b/dev-packages/e2e-tests/test-applications/nitro-3/package.json index 3bfee836718a..220276c63139 100644 --- a/dev-packages/e2e-tests/test-applications/nitro-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nitro-3/package.json @@ -21,7 +21,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "latest || *", "nitro": "^3.0.260522-beta", "rolldown": "latest", diff --git a/dev-packages/e2e-tests/test-applications/node-eve/package.json b/dev-packages/e2e-tests/test-applications/node-eve/package.json index 3b45ee182d4e..978dc8a2dbd8 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/package.json +++ b/dev-packages/e2e-tests/test-applications/node-eve/package.json @@ -32,8 +32,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", + "@sentry/conventions": "0.32.0", "@types/node": "24.x", "typescript": "~5.9.0" }, diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 446589a1db0c..3c880974fe0b 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -20,6 +20,8 @@ const NODE_EXPORTS_IGNORE = [ 'initWithoutDefaultIntegrations', // Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration) '_INTERNAL_normalizeCollectionInterval', + // Only needed by framework SDKs that enable the managed tunnel after `init` + '_INTERNAL_installTunnelHooks', // not exported by bun 'nativeNodeFetchIntegration', ]; diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json index b7e349125d37..9925a989d79e 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz", "express": "^4.21.2", "mysql": "^2.18.1" }, diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json index fb5517e65d30..006775311d31 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/opentelemetry": "file:../../packed/sentry-opentelemetry-packed.tgz", "express": "^4.21.2" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json index 4dda82a346a3..dc23a20a86a7 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v1/package.json @@ -21,8 +21,7 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz" + "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" }, "type": "module", "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json index b0d70c570d18..6482c7f3a8e0 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/package.json @@ -24,8 +24,7 @@ "devDependencies": { "@modelcontextprotocol/client": "^2.0.0", "@playwright/test": "1.64.1", - "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz" + "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" }, "type": "module", "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/package.json b/dev-packages/e2e-tests/test-applications/node-firebase/package.json index 9bab9dc4d32c..bc674ca3a93b 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/package.json +++ b/dev-packages/e2e-tests/test-applications/node-firebase/package.json @@ -15,8 +15,6 @@ }, "dependencies": { "@types/node": "^22.13.14", - "dotenv": "^16.4.5", - "tsconfig-paths": "^4.2.0", "typescript": "5.9.3" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/node-flue/package.json b/dev-packages/e2e-tests/test-applications/node-flue/package.json index eb7949b020d5..21fd3fd9a5bb 100644 --- a/dev-packages/e2e-tests/test-applications/node-flue/package.json +++ b/dev-packages/e2e-tests/test-applications/node-flue/package.json @@ -26,7 +26,6 @@ "@flue/vite": "2.0.5", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "24.x", "typescript": "~5.9.0", "vite": "^8.0.14" diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/package.json b/dev-packages/e2e-tests/test-applications/node-hapi/package.json index 8a7725dad0b5..70e17ef2e6f1 100644 --- a/dev-packages/e2e-tests/test-applications/node-hapi/package.json +++ b/dev-packages/e2e-tests/test-applications/node-hapi/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" } } diff --git a/dev-packages/e2e-tests/test-applications/node-mastra/package.json b/dev-packages/e2e-tests/test-applications/node-mastra/package.json index b24a01edc9be..d2cf7f675fc3 100644 --- a/dev-packages/e2e-tests/test-applications/node-mastra/package.json +++ b/dev-packages/e2e-tests/test-applications/node-mastra/package.json @@ -29,7 +29,7 @@ "@mastra/deployer": "~1.74.0", "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@types/node": "24.x", "mastra": "~1.28.0", "typescript": "~5.9.0" diff --git a/dev-packages/e2e-tests/test-applications/node-openai-decisions/package.json b/dev-packages/e2e-tests/test-applications/node-openai-decisions/package.json index 41a84ca03d45..94125ec7a332 100644 --- a/dev-packages/e2e-tests/test-applications/node-openai-decisions/package.json +++ b/dev-packages/e2e-tests/test-applications/node-openai-decisions/package.json @@ -24,7 +24,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@types/express": "^4.17.21", "@types/node": "^24.0.0", "typescript": "~5.9.0" diff --git a/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json b/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json index 7f21e622f083..2f2fdcc528ae 100644 --- a/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json +++ b/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json @@ -20,8 +20,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.30.0", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", + "@sentry/conventions": "0.32.0", "@types/node": "24.x" }, "engines": { diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json index 4db8e675ec8e..91bd2f41e685 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-cjs/package.json @@ -10,7 +10,6 @@ "test:assert": "pnpm run typecheck && pnpm run test" }, "dependencies": { - "@playwright/test": "1.64.1", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@sentry/node": "file:../../packed/sentry-node-packed.tgz", "@sentry/profiling-node": "file:../../packed/sentry-profiling-node-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json index 3753211523c6..a2e3f977d786 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json @@ -10,7 +10,6 @@ "test:assert": "pnpm run typecheck && pnpm run test" }, "dependencies": { - "@playwright/test": "1.64.1", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@sentry/node": "file:../../packed/sentry-node-packed.tgz", "@sentry/profiling-node": "file:../../packed/sentry-profiling-node-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-rolldown/package.json b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json index cb74b43858f5..67f5415a49d1 100644 --- a/dev-packages/e2e-tests/test-applications/node-rolldown/package.json +++ b/dev-packages/e2e-tests/test-applications/node-rolldown/package.json @@ -10,8 +10,7 @@ "test:assert": "node ./assert.mjs" }, "dependencies": { - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz" + "@sentry/node": "file:../../packed/sentry-node-packed.tgz" }, "devDependencies": { "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-rollup/package.json b/dev-packages/e2e-tests/test-applications/node-rollup/package.json index 86267a84fa07..11dd0fd10088 100644 --- a/dev-packages/e2e-tests/test-applications/node-rollup/package.json +++ b/dev-packages/e2e-tests/test-applications/node-rollup/package.json @@ -10,8 +10,7 @@ "test:assert": "node ./assert.mjs" }, "dependencies": { - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz" + "@sentry/node": "file:../../packed/sentry-node-packed.tgz" }, "devDependencies": { "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-vite/package.json b/dev-packages/e2e-tests/test-applications/node-vite/package.json index 48738a440a3f..8df6411fb907 100644 --- a/dev-packages/e2e-tests/test-applications/node-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/node-vite/package.json @@ -10,8 +10,7 @@ "test:assert": "node ./assert.mjs" }, "dependencies": { - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz" + "@sentry/node": "file:../../packed/sentry-node-packed.tgz" }, "devDependencies": { "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/node-webpack/package.json b/dev-packages/e2e-tests/test-applications/node-webpack/package.json index 1efc6ec0ff88..26fa1ce27665 100644 --- a/dev-packages/e2e-tests/test-applications/node-webpack/package.json +++ b/dev-packages/e2e-tests/test-applications/node-webpack/package.json @@ -10,8 +10,7 @@ "test:assert": "node ./assert.mjs" }, "dependencies": { - "@sentry/node": "file:../../packed/sentry-node-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz" + "@sentry/node": "file:../../packed/sentry-node-packed.tgz" }, "devDependencies": { "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/post-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/post-error.ts new file mode 100644 index 000000000000..42a0d62de815 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/post-error.ts @@ -0,0 +1,6 @@ +import { defineEventHandler } from '#imports'; + +// Throws, so a test can check what request data the error event carries. +export default defineEventHandler(() => { + throw new Error('POST error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts new file mode 100644 index 000000000000..aaca20e2a3eb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant'); + +test('keeps the body of a request that is not a tunnel request off its error event', async ({ request }) => { + const errorPromise = waitForError('nuxt-4-cloudflare', event => event.exception?.values?.[0]?.value === 'POST error'); + + await request.post('/app/api/post-error', { data: { note: 'stays off the event' } }); + const error = await errorPromise; + + expect(error.request?.method).toBe('POST'); + expect(error.request?.data).toBeUndefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json index f62cc242e350..2f318a648dd3 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json @@ -26,7 +26,7 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" }, "sentryTest": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/app/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/app/app.vue new file mode 100644 index 000000000000..edd3b5d91e9a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/app/app.vue @@ -0,0 +1,13 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/nuxt.config.ts new file mode 100644 index 000000000000..eebfc18d5191 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/nuxt.config.ts @@ -0,0 +1,18 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2025-06-06', + modules: ['@sentry/nuxt/module'], + // The tunnel variant serves the app, and so the tunnel, under a base URL. + ...(process.env.E2E_TEST_TUNNEL && { app: { baseURL: '/app/' } }), + nitro: { + preset: 'cloudflare_module', + cloudflare: { + nodeCompat: true, + deployConfig: false, + }, + // The tunnel variant serves the managed tunnel. + replace: { + __E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL), + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/package.json new file mode 100644 index 000000000000..cef0544fdde3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/package.json @@ -0,0 +1,41 @@ +{ + "name": "nuxt-5-cloudflare", + "type": "module", + "private": true, + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "preview": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030 --log-level=$(test $CI && echo 'none' || echo 'log')", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test", + "test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 pnpm build", + "test:assert:tunnel": "E2E_TEST_TUNNEL=1 pnpm test tunnel" + }, + "//": [ + "Nuxt v5 is only on the Nuxt nightlies, like the `nuxt-5` app.", + "`ofetch` is a dependency of `nuxt`, which Nuxt's virtual `fetch.server.mjs` can't resolve from the app, see `nuxt-5`." + ], + "dependencies": { + "@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz", + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "nuxt": "npm:nuxt-nightly@5x", + "ofetch": "^2.0.0-alpha.3", + "vue": "^3.5.41" + }, + "devDependencies": { + "@playwright/test": "1.64.1", + "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", + "wrangler": "^4.72.0" + }, + "sentryTest": { + "optional": true, + "variants": [ + { + "build-command": "pnpm test:build:tunnel", + "assert-command": "pnpm test:assert:tunnel", + "label": "nuxt-5-cloudflare (tunnel)" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/playwright.config.mjs new file mode 100644 index 000000000000..4a688103e068 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/playwright.config.mjs @@ -0,0 +1,5 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ startCommand: 'pnpm preview', port: 3030 }); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/pnpm-workspace.yaml b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/pnpm-workspace.yaml new file mode 100644 index 000000000000..fd29a82085e0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + esbuild: true + workerd: true +minimumReleaseAge: 0 diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/anchor.ts b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/anchor.ts new file mode 100644 index 000000000000..63c9ceb606e9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/anchor.ts @@ -0,0 +1,4 @@ +import { defineHandler } from 'nitro'; + +// A plain route for tests to request after the tunnel traffic. +export default defineHandler(() => 'ok'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/internal.ts b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/internal.ts new file mode 100644 index 000000000000..fd0ec635de08 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/internal.ts @@ -0,0 +1,4 @@ +import { defineHandler } from 'nitro'; + +// Fetched internally while the page renders on the server. +export default defineHandler(() => 'internal'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/plugins/sentry.ts b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/plugins/sentry.ts new file mode 100644 index 000000000000..8368a5a85dde --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/plugins/sentry.ts @@ -0,0 +1,15 @@ +import { definePlugin } from 'nitro'; +import { serveTunnelIntegration } from '@sentry/cloudflare'; +import { sentryCloudflareNitroPlugin } from '@sentry/nuxt/module/plugins'; + +declare const __E2E_TEST_TUNNEL__: boolean; + +export default definePlugin( + sentryCloudflareNitroPlugin({ + // The tunnel forwards to the DSN host, which is the event proxy in the tunnel variant. + dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server + integrations: __E2E_TEST_TUNNEL__ ? [serveTunnelIntegration()] : [], + }), +); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/start-event-proxy.mjs new file mode 100644 index 000000000000..569c8ee47365 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nuxt-5-cloudflare', +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/internal-fetch.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/internal-fetch.test.ts new file mode 100644 index 000000000000..54a57df9166e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/internal-fetch.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; + +function getUrlPath(span: SerializedStreamedSpan): string | undefined { + const attribute = span.attributes['url.path']; + + return attribute?.type === 'string' ? attribute.value : undefined; +} + +test('does not instrument the internal `$fetch` of a server-rendered page as its own request', async ({ request }) => { + // One listener, so it gets every span that the proxy received before the barrier span. + const spans: SerializedStreamedSpan[] = []; + const barrierPromise = waitForStreamedSpans('nuxt-5-cloudflare', batch => { + spans.push(...batch); + return batch.some(span => span.is_segment && getUrlPath(span) === '/api/anchor'); + }); + + await request.get('/'); + await request.get('/api/anchor'); + await barrierPromise; + + const serverSpanPaths = spans.filter(span => getSpanOp(span) === 'http.server').map(getUrlPath); + expect(serverSpanPaths).toContain('/'); + expect(serverSpanPaths).not.toContain('/api/internal'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/tunnel.test.ts new file mode 100644 index 000000000000..8c54796751c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/tunnel.test.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test'; +import { isServerSpanAt, makeClientReportEnvelope, watchForTunnelSpan } from '@sentry-internal/test-utils'; + +test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant'); + +// The isolate's first tunnel request is the one a late span filter would let through, so this stays the only test. +test('answers the managed tunnel set up by `sentryCloudflareNitroPlugin`, without a span of it', async ({ + request, +}) => { + const dsn = 'http://public@localhost:3031/1337'; + const raceTunnelSpan = watchForTunnelSpan('nuxt-5-cloudflare', isServerSpanAt(/^\/app\/qpo967/)); + + // The path the SDK derives from `dsn`. + const response = await request.post('/app/qpo967', { + data: makeClientReportEnvelope(dsn), + headers: { 'Content-Type': 'application/x-sentry-envelope' }, + }); + + expect(response.status()).toBe(200); + expect(await raceTunnelSpan(isServerSpanAt('/app/api/anchor'), () => request.get('/app/api/anchor'))).toBe('anchor'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tsconfig.json new file mode 100644 index 000000000000..a746f2a70c28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/wrangler.jsonc new file mode 100644 index 000000000000..da71d4978e33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/wrangler.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "nuxt-5-cloudflare", + "main": "./.output/server/index.mjs", + "compatibility_date": "2026-06-29", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": "./.output/public", + "binding": "ASSETS", + }, +} diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json index 157db2de17d6..60295996937e 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/package.json +++ b/dev-packages/e2e-tests/test-applications/react-19/package.json @@ -4,8 +4,6 @@ "private": true, "dependencies": { "@sentry/react": "file:../../packed/sentry-react-packed.tgz", - "history": "4.9.0", - "@types/history": "4.7.11", "@types/node": "^18.19.1", "@types/react": "npm:types-react@rc", "@types/react-dom": "npm:types-react-dom@rc", diff --git a/dev-packages/e2e-tests/test-applications/react-router-5/package.json b/dev-packages/e2e-tests/test-applications/react-router-5/package.json index 34d06520e4e9..1ceea6c66961 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-5/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-5/package.json @@ -9,7 +9,6 @@ "@types/node": "^18.19.1", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", - "@types/react-router": "5.1.20", "@types/react-router-dom": "5.3.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json index a546ef6ddb59..cfe125b443cb 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json @@ -6,7 +6,6 @@ "@sentry/react": "file:../../packed/sentry-react-packed.tgz", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", - "express": "^4.21.2", "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "7.6.0", @@ -43,7 +42,6 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "serve": "14.0.1", - "npm-run-all2": "^6.2.0" + "serve": "14.0.1" } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json index ad3dd46d1d6c..0a1ea2c6b4d6 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json @@ -9,7 +9,6 @@ "@react-router/serve": "^7", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "ioredis": "5.10.1", - "isbot": "^5.1.17", "mysql": "^2.18.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json index 6c93eacbf3d3..006f6f9a43d1 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/package.json @@ -19,7 +19,6 @@ "dependencies": { "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "@react-router/node": "^7.13.0", - "@react-router/serve": "^7.13.0", "isbot": "^5.1.27", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -29,14 +28,12 @@ "@playwright/test": "1.64.1", "@react-router/dev": "^7.13.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@tailwindcss/vite": "^4.1.4", "@types/node": "^20", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "tailwindcss": "^4.1.4", "typescript": "^5.8.3", - "vite": "^6.4.2", - "vite-tsconfig-paths": "^5.1.4" + "vite": "^6.4.2" }, "browserslist": { "production": [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/package.json index 7a5ccf0d4c6a..f8b70347a427 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/package.json @@ -10,8 +10,7 @@ "@react-router/node": "^7.13.0", "@react-router/serve": "^7.13.0", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "ioredis": "^5.4.1", - "isbot": "^5.1.17" + "ioredis": "^5.4.1" }, "devDependencies": { "@types/react": "18.3.1", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json index e0395c3ccad8..d161cb4a577c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json @@ -10,8 +10,7 @@ "@react-router/node": "^7.13.0", "@react-router/serve": "^7.13.0", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "ioredis": "^5.4.1", - "isbot": "^5.1.17" + "ioredis": "^5.4.1" }, "devDependencies": { "@types/react": "18.3.1", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json index c0525a3ddc64..4c0333dac6bf 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/package.json @@ -6,7 +6,6 @@ "@sentry/react": "file:../../packed/sentry-react-packed.tgz", "@types/react": "18.0.0", "@types/react-dom": "18.0.0", - "express": "^4.21.2", "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "7.6.0", @@ -43,7 +42,6 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "serve": "14.0.1", - "npm-run-all2": "^6.2.0" + "serve": "14.0.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 c1acf56da8ac..90284b409347 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 @@ -200,9 +200,14 @@ const router = sentryCreateBrowserRouter( declare global { interface Window { __REACT_ROUTER__: typeof router; + __SENTRY_HAS_ACTIVE_NAVIGATION__: () => boolean; } } window.__REACT_ROUTER__ = router; +window.__SENTRY_HAS_ACTIVE_NAVIGATION__ = () => { + const span = Sentry.getActiveSpan(); + return !!span && Sentry.spanToJSON(span).attributes['sentry.op'] === 'navigation'; +}; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render(); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/spans.test.ts index e5aed8fda13b..8fbe2c0701d8 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/spans.test.ts @@ -423,8 +423,7 @@ test('Updates pageload span name correctly when span is cancelled early (documen test('Updates navigation span name correctly when span is cancelled early (document.hidden simulation)', async ({ page, }) => { - // First go to home page - await page.goto('/'); + await page.goto('/?idleTimeout=5000'); const navigationPromise = waitForStreamedSpan('react-router-7-lazy-routes', span => { return ( @@ -432,60 +431,24 @@ test('Updates navigation span name correctly when span is cancelled early (docum ); }); - // Set up a listener to simulate document.hidden after clicking the navigation link + await page.locator('id=navigation').click(); + await page.waitForFunction(() => + (window as unknown as { __SENTRY_HAS_ACTIVE_NAVIGATION__: () => boolean }).__SENTRY_HAS_ACTIVE_NAVIGATION__(), + ); + await page.evaluate(() => { - // Override document.hidden to simulate tab switching - let hiddenValue = false; - Object.defineProperty(document, 'hidden', { - configurable: true, - get: function () { - return hiddenValue; - }, - }); - - // Listen for clicks on the navigation link and simulate document.hidden shortly after - document.addEventListener( - 'click', - () => { - setTimeout(() => { - hiddenValue = true; - // Dispatch visibilitychange event to trigger the idle span cancellation logic - document.dispatchEvent(new Event('visibilitychange')); - }, 50); // Small delay to ensure the navigation span has started - }, - { once: true }, - ); + Object.defineProperty(document, 'hidden', { configurable: true, value: true }); + document.dispatchEvent(new Event('visibilitychange')); }); - // Click the navigation link to navigate to the lazy route - const navigationLink = page.locator('id=navigation'); - await expect(navigationLink).toBeVisible(); - await navigationLink.click(); - const event = await navigationPromise; + await expect(page.locator('id=innermost-lazy-route')).toBeVisible(); - // Verify the lazy route content eventually loads (even though span was cancelled early) - const lazyRouteContent = page.locator('id=innermost-lazy-route'); - await expect(lazyRouteContent).toBeVisible(); - - // Validate that the transaction event has the correct parameterized route name - // even though the span was cancelled early due to document.hidden expect(event.name).toBe('/lazy/inner/:id/:anotherId/:someAnotherId'); expect(event.is_segment).toBe(true); expect(getSpanOp(event)).toBe('navigation'); - - // Check if the span was indeed cancelled (should have cancellation_reason attribute or idle_span_finish_reason) - const cancellationReason = event.attributes['sentry.cancellation_reason']?.value; - const idleSpanFinishReason = event.attributes['sentry.idle_span_finish_reason']?.value; - - // Verify that the span was cancelled due to document.hidden - if (cancellationReason) { - expect(cancellationReason).toBe('document.hidden'); - } - - if (idleSpanFinishReason) { - expect(['externalFinish', 'cancelled']).toContain(idleSpanFinishReason); - } + expect(event.attributes['sentry.cancellation_reason']?.value).toBe('document.hidden'); + expect(event.attributes['sentry.idle_span_finish_reason']?.value).toBe('externalFinish'); }); test('Creates separate spans for rapid consecutive navigations', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/package.json index 9367f20cc478..2b38c7265bc9 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/package.json @@ -6,7 +6,6 @@ "@sentry/react": "file:../../packed/sentry-react-packed.tgz", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", - "express": "^4.21.2", "react": "19.2.7", "react-dom": "19.2.7", "react-router": "^8.0.0", @@ -43,7 +42,6 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "serve": "14.0.1", - "npm-run-all2": "^6.2.0" + "serve": "14.0.1" } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json index 151640418a31..282a31c2fa37 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json @@ -15,7 +15,6 @@ "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "ioredis": "^5.11.1", - "isbot": "^5.1.43", "mysql": "^2.18.1" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json index f14be2a9bcbe..f92cabfd63e5 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -6,7 +6,6 @@ "build": "pnpm typecheck && shopify hydrogen build --codegen", "dev": "shopify hydrogen dev --codegen", "preview": "shopify hydrogen preview", - "lint": "eslint --no-error-on-unmatched-pattern --ext .js,.ts,.jsx,.tsx .", "typecheck": "tsc", "codegen": "shopify hydrogen codegen", "clean": "npx rimraf node_modules dist pnpm-lock.yaml", @@ -19,11 +18,9 @@ "@remix-run/cloudflare-pages": "^2.17.4", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz", - "@sentry/vite-plugin": "^5.4.1", "@shopify/hydrogen": "2025.4.0", "@shopify/remix-oxygen": "2.0.10", "graphql": "^16.6.0", - "graphql-tag": "^2.12.6", "isbot": "^3.8.0", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -32,21 +29,16 @@ "@graphql-codegen/cli": "5.0.2", "@playwright/test": "1.64.1", "@remix-run/dev": "^2.17.4", - "@remix-run/eslint-config": "^2.17.4", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@shopify/cli": "3.74.1", "@shopify/hydrogen-codegen": "^0.3.1", "@shopify/mini-oxygen": "3.2.0", "@shopify/oxygen-workers-types": "^4.1.2", - "@tailwindcss/vite": "4.0.0-alpha.17", "@total-typescript/ts-reset": "^0.4.2", - "@types/eslint": "^8.4.10", "@types/node": "^18.19.1", "@types/react": "^18.2.22", "@types/react-dom": "^18.2.7", "esbuild": "0.25.0", - "eslint": "^8.20.0", - "eslint-plugin-hydrogen": "0.12.2", "typescript": "^5.2.2", "vite": "^5.1.0", "vite-tsconfig-paths": "^4.3.1" diff --git a/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json b/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json index ee150587205c..03028eec89a6 100644 --- a/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-server-timing/package.json @@ -27,7 +27,6 @@ "@remix-run/eslint-config": "2.17.4", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.34", - "@types/prop-types": "15.7.7", "eslint": "^8.38.0", "typescript": "^5.1.6", "vite": "^5.4.11", diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/package.json b/dev-packages/e2e-tests/test-applications/remix-v3/package.json index 7d52c7d0d8a1..f24799a42e64 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-v3/package.json @@ -19,7 +19,6 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "^24.6.0", "typescript": "~5.9.0" }, diff --git a/dev-packages/e2e-tests/test-applications/solid-static/package.json b/dev-packages/e2e-tests/test-applications/solid-static/package.json index 8cf9909a537a..257ccefe49c2 100644 --- a/dev-packages/e2e-tests/test-applications/solid-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solid-static/package.json @@ -18,7 +18,6 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "autoprefixer": "^10.4.17", "postcss": "^8.4.33", - "solid-devtools": "^0.29.2", "tailwindcss": "^3.4.1", "vite": "^5.4.11", "vite-plugin-solid": "^2.11.6" diff --git a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json index 860c5c299dbc..dd594fac5265 100644 --- a/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json @@ -16,8 +16,6 @@ "@sentry/solid": "file:../../packed/sentry-solid-packed.tgz", "@tailwindcss/vite": "^4.0.6", "@tanstack/solid-router": "^1.141.8", - "@tanstack/solid-router-devtools": "^1.132.25", - "@tanstack/solid-start": "^1.132.25", "solid-js": "^1.9.5", "tailwindcss": "^4.0.6" }, diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json index d3224e8db4bd..1ba8e09c2ac7 100644 --- a/dev-packages/e2e-tests/test-applications/solid/package.json +++ b/dev-packages/e2e-tests/test-applications/solid/package.json @@ -18,7 +18,6 @@ "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "autoprefixer": "^10.4.17", "postcss": "^8.4.33", - "solid-devtools": "^0.29.2", "tailwindcss": "^3.4.1", "vite": "^5.4.11", "vite-plugin-solid": "^2.11.6" diff --git a/dev-packages/e2e-tests/test-applications/solidstart-2/package.json b/dev-packages/e2e-tests/test-applications/solidstart-2/package.json index f0c55b3ba846..7794cea772f2 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-2/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-2/package.json @@ -25,14 +25,10 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "@solidjs/start": "2.0.2", - "@solidjs/testing-library": "^0.8.10", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0", "solid-js": "1.9.14", "typescript": "^5.4.5", - "vite": "^8.1.5", - "vite-plugin-solid": "^2.11.6" + "vite": "^8.1.5" }, "engines": { "node": ">=24" diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json index 7dfcebfffef2..d306c1edd00c 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json @@ -20,14 +20,10 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "@solidjs/start": "^1.0.2", - "@solidjs/testing-library": "^0.8.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0", "solid-js": "1.9.5", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.4.10", - "vite-plugin-solid": "^2.11.6" + "vite": "^5.4.10" } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index e8f890d6434d..7e9d04cbd820 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -20,14 +20,10 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "@solidjs/start": "^1.0.2", - "@solidjs/testing-library": "^0.8.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0", "solid-js": "1.9.5", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.4.11", - "vite-plugin-solid": "^2.11.6" + "vite": "^5.4.11" } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json index 1952ea19d33a..5a73c4c69571 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -18,19 +18,15 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "@solidjs/start": "^1.0.2", - "@solidjs/testing-library": "^0.8.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0", "solid-js": "1.9.5", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.4.11", - "vite-plugin-solid": "^2.11.6" + "vite": "^5.4.11" } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json index 2b266bfdeb9b..ddbed77f2895 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json @@ -20,14 +20,10 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "@solidjs/start": "^1.0.2", - "@solidjs/testing-library": "^0.8.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0", "solid-js": "1.9.5", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.4.11", - "vite-plugin-solid": "^2.11.6" + "vite": "^5.4.11" } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index daceab6d5f10..58542cf48282 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -27,15 +27,11 @@ "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", "@solidjs/start": "^1.0.2", - "@solidjs/testing-library": "^0.8.7", "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0", "solid-js": "1.9.5", "typescript": "^5.4.5", "vinxi": "^0.5.11", - "vite": "^6.4.4", - "vite-plugin-solid": "^2.11.6" + "vite": "^6.4.4" }, "sentryTest": { "variants": [ diff --git a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json index ba96bf446930..20bdfe59a5d7 100644 --- a/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json +++ b/dev-packages/e2e-tests/test-applications/supabase-nextjs/package.json @@ -13,7 +13,6 @@ "test:assert": "pnpm test:prod" }, "dependencies": { - "@next/font": "14.2.15", "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", "@supabase/auth-helpers-react": "0.5.0", "@supabase/auth-ui-react": "0.4.7", @@ -30,9 +29,7 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "eslint": "8.34.0", - "eslint-config-next": "14.2.25" + "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" }, "sentryTest": { "optional": true diff --git a/dev-packages/e2e-tests/test-applications/svelte-5-static/package.json b/dev-packages/e2e-tests/test-applications/svelte-5-static/package.json index 02c83d3b83ad..f45331a652f7 100644 --- a/dev-packages/e2e-tests/test-applications/svelte-5-static/package.json +++ b/dev-packages/e2e-tests/test-applications/svelte-5-static/package.json @@ -19,7 +19,6 @@ "@tsconfig/svelte": "^5.0.2", "svelte": "^5.0.0-next.115", "svelte-check": "^3.6.7", - "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^5.4.11" }, diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json index 98d6261ce47b..b820e6b140b3 100644 --- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json @@ -19,7 +19,6 @@ "@tsconfig/svelte": "^5.0.2", "svelte": "^5.0.0-next.115", "svelte-check": "^3.6.7", - "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^5.4.11" }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json index c3b4ce462190..f9f2d237d163 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json @@ -26,7 +26,6 @@ "@sveltejs/vite-plugin-svelte": "^6.1.3", "svelte": "^5.38.3", "svelte-check": "^4.3.1", - "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^7.3.2" }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-otlp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-otlp/package.json index cc17b79e7d20..8812b0dd9400 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-otlp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-otlp/package.json @@ -31,7 +31,6 @@ "@sveltejs/vite-plugin-svelte": "^6.1.3", "svelte": "^5.38.3", "svelte-check": "^4.3.1", - "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^7.3.2" }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json index f9a220037668..c4686c849dd6 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json @@ -27,7 +27,6 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", "@sveltejs/kit": "2.70.2", "@sveltejs/vite-plugin-svelte": "^3.0.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index 45b5405a3433..c9d61dd8087a 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -26,7 +26,6 @@ "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^5.0.0-next.115", "svelte-check": "^3.6.0", - "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^5.4.11" }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json index 18092c2dfef8..765d71dccb12 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2.5.0-twp/package.json @@ -25,7 +25,6 @@ "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^5.0.0-next.115", "svelte-check": "^3.6.0", - "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^5.4.11" }, diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 1ca6fd124a5e..9a4bdcbd38bb 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -27,7 +27,6 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", "@sveltejs/kit": "2.60.1", "@sveltejs/vite-plugin-svelte": "^3.0.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-3-cloudflare-workers/package.json index 62446e94a44f..688124f3c2e2 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-3-cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-cloudflare-workers/package.json @@ -24,7 +24,6 @@ "@sveltejs/adapter-cloudflare": "^8.0.0", "@sveltejs/kit": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^7.3.1", - "cookie": "^2.0.1", "svelte": "^5.57.1", "typescript": "^6.0.0", "vite": "^8.0.12", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/effect-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/effect-fetch/+page.svelte new file mode 100644 index 000000000000..0176ad4d6bb6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/effect-fetch/+page.svelte @@ -0,0 +1,13 @@ + + +

responses: {responses}

+Same route, new query +Leave page diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/tracing.client.test.ts index 47aa28294a58..08e7255b145f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/tracing.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/tracing.client.test.ts @@ -77,3 +77,23 @@ test.describe('client-specific performance events', () => { ); }); }); + +test('fetch instrumentation does not make effects re-run on navigation', async ({ page }) => { + const effectFetches: string[] = []; + page.on('request', request => { + if (new URL(request.url()).pathname === '/api/users') { + effectFetches.push(request.url()); + } + }); + + await waitForInitialPageload(page, { route: '/effect-fetch' }); + await expect(page.locator('#effect-fetch-responses')).toHaveText('responses: 1'); + + await page.getByText('Same route, new query').click(); + await page.waitForURL('**/effect-fetch?q=1'); + + await page.getByText('Leave page').click(); + await expect(page.getByText('Navigation 1')).toBeVisible(); + + expect(effectFetches).toHaveLength(1); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json index b7599df88450..cd89ab9a134f 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json @@ -22,8 +22,6 @@ "devDependencies": { "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "typescript": "^5.2.2", "vite": "^5.4.11", diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json index e14277b3ff56..ccc30c471ab9 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json @@ -37,8 +37,6 @@ "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@types/node": "^24.10.0", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^4.3.3", "typescript": "^5.9.0", "vite": "8.3.4", diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json index 69c718ec1dee..14566ba22a7e 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -39,8 +39,6 @@ "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@types/node": "^24.10.0", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^4.3.3", "typescript": "^5.9.0", "vite": "8.3.4", diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts index d42510f36173..b845965f5caf 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/tunnel.test.ts @@ -61,15 +61,6 @@ test('Sends client-side errors through the configured tunnel route', async ({ pa expect(errorEvent.transaction).toBe(`${basePath}/`); }); -test('Rejects non-POST requests to the managed tunnel route instead of rendering the app', async ({ request }) => { - test.skip(tunnelRouteMode !== 'static', 'Requires a known managed tunnel path'); - - const response = await request.get('/monitor'); - - expect(response.status()).toBe(405); - expect(response.headers()['allow']).toBe('POST'); -}); - function pathnameMatchesTunnelRoute(pathname: string): boolean { return typeof expectedTunnelPathMatcher === 'string' ? pathname === expectedTunnelPathMatcher diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/package.json b/dev-packages/e2e-tests/test-applications/vue-3-static/package.json index ea53e5e51aaa..a16086c0109a 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3-static/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/package.json @@ -34,7 +34,6 @@ "@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vue/tsconfig": "^0.5.1", - "http-server": "^14.1.1", "npm-run-all2": "^6.2.0", "typescript": "~5.3.0", "vite": "^5.4.11", diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index 6048d7d8d9ff..abbc7e01d4eb 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -37,7 +37,6 @@ "@vitejs/plugin-vue": "^5.0.3", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vue/tsconfig": "^0.5.1", - "http-server": "^14.1.1", "npm-run-all2": "^6.2.0", "typescript": "~5.3.0", "vite": "^5.4.11", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 75266ac2cc94..1cff6a9ffb50 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -14,8 +14,7 @@ "build:dev": "pnpm run build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", - "clean": "rimraf -g suites/**/node_modules suites/**/tmp_* && pnpm run clean:script", - "clean:script": "node scripts/clean.js", + "clean": "node scripts/clean.js", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", "type-check": "pnpm run type-check:src && pnpm run type-check:test", @@ -51,9 +50,9 @@ "@langchain/langgraph": "^0.2.32", "@langchain/openai": "^0.5.0", "@mistralai/mistralai": "3.3.0", - "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/client": "^2.4.0", "@modelcontextprotocol/sdk": "1.32.2", - "@modelcontextprotocol/server": "^2.0.0", + "@modelcontextprotocol/server": "^2.4.0", "@neondatabase/serverless": "1.2.0", "@prisma/adapter-pg": "7.2.0", "@prisma/client": "6.15.0", @@ -61,27 +60,28 @@ "@sentry-internal/rollup-utils": "11.11.0", "@sentry-internal/test-utils": "11.11.0", "@sentry/aws-serverless": "11.11.0", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", "@sentry/node": "11.11.0", "@sentry/node-native": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", + "@smithy/core": "3.38.1", "@smithy/node-http-handler": "4.12.1", "@types/express": "^4.17.21", "@types/node-schedule": "^2.1.8", "@typesafe-ai/sdk": "^0.6.0", "ai": "^4.3.16", "amqplib": "^0.10.9", - "body-parser": "^2.3.0", + "body-parser": "^2.4.0", "bullmq": "^5.79.1", "consola": "^3.2.3", "cors": "^2.8.5", "cron": "^4.4.0", "dataloader": "2.2.3", "esbuild": "0.28.1", - "express": "^4.21.2", + "express": "^4.22.3", "fastify": "^5.12.7", "generic-pool": "^3.9.0", "graphql": "^16.11.0", diff --git a/dev-packages/node-integration-tests/scripts/clean.js b/dev-packages/node-integration-tests/scripts/clean.js index fc5a09092a19..1c0f4f27c095 100644 --- a/dev-packages/node-integration-tests/scripts/clean.js +++ b/dev-packages/node-integration-tests/scripts/clean.js @@ -1,11 +1,16 @@ const { execSync } = require('child_process'); +const { globSync, rmSync } = require('fs'); + +for (const path of globSync(['suites/**/node_modules', 'suites/**/tmp_*'])) { + rmSync(path, { recursive: true, force: true }); +} // The runner (utils/runner/createRunner.ts) starts each suite's docker compose // stack under a project name prefixed with `sentry-it-`, derived by hashing the // working directory. Some suites (e.g. the Prisma tests) run from a temporary // directory whose path — and therefore whose derived project name — isn't known // ahead of time and no longer exists on disk by the time this script runs (the -// `clean` npm script removes `tmp_*` dirs first). Reconstructing project names +// `tmp_*` dirs are removed above). Reconstructing project names // from compose file paths therefore misses those stacks and leaks containers. // // Instead, ask Docker for every `sentry-it-*` project it still knows about and diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/scenario-stream.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/scenario-stream.mjs new file mode 100644 index 000000000000..aaa3ecb93ed0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/scenario-stream.mjs @@ -0,0 +1,200 @@ +import * as Sentry from '@sentry/aws-serverless'; +import { + BedrockRuntimeClient, + ConverseStreamCommand, + InvokeModelWithResponseStreamCommand, +} from '@aws-sdk/client-bedrock-runtime'; +// Force the HTTP/1 handler so `nock` can intercept the request instead of hitting real AWS. +import { NodeHttpHandler } from '@smithy/node-http-handler'; +import { EventStreamCodec } from '@smithy/core/event-streams'; +import nock from 'nock'; +import { Buffer } from 'node:buffer'; + +nock.disableNetConnect(); + +const region = 'us-east-1'; +const credentials = { accessKeyId: 'aws-test-key', secretAccessKey: 'aws-test-secret' }; +const host = `https://bedrock-runtime.${region}.amazonaws.com`; +const modelId = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; + +// Bedrock streams responses in the binary AWS event stream format, not JSON. The SDK decodes and +// checks each message (headers, payload, CRC32), so the mocked response must use the same format. +// The SDK's own codec encodes the messages. It needs UTF-8 helpers to encode the header strings. +// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTSelectObjectAppendix.html +const codec = new EventStreamCodec( + bytes => Buffer.from(bytes).toString('utf8'), + text => Buffer.from(text), +); + +// The codec needs a type for each header value. All Bedrock headers are strings. +function encodeMessage(headers, payload) { + return codec.encode({ + headers: Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, { type: 'string', value }])), + body: Buffer.from(JSON.stringify(payload)), + }); +} + +// The SDK reads `:event-type` to find the stream member (for example `messageStart` or `chunk`). +function event(eventType, payload) { + return encodeMessage( + { ':message-type': 'event', ':event-type': eventType, ':content-type': 'application/json' }, + payload, + ); +} + +// An exception message makes the SDK throw while the caller iterates the stream. This is how a +// stream fails after the response starts. +function exception(exceptionType, payload) { + return encodeMessage( + { ':message-type': 'exception', ':exception-type': exceptionType, ':content-type': 'application/json' }, + payload, + ); +} + +// InvokeModelWithResponseStream wraps each model chunk as base64 JSON in a `chunk` event. +function invokeModelChunk(chunk) { + return event('chunk', { bytes: Buffer.from(JSON.stringify(chunk)).toString('base64') }); +} + +function mockStream(path, messages) { + nock(host).post(path).reply(200, Buffer.concat(messages), { 'content-type': 'application/vnd.amazon.eventstream' }); +} + +function createClient() { + return new BedrockRuntimeClient({ region, credentials, maxAttempts: 1, requestHandler: new NodeHttpHandler() }); +} + +async function converseStream() { + mockStream(/\/model\/.*\/converse-stream$/, [ + event('messageStart', { role: 'assistant' }), + event('contentBlockDelta', { contentBlockIndex: 0, delta: { text: 'Hello ' } }), + event('contentBlockDelta', { contentBlockIndex: 0, delta: { text: 'from Bedrock' } }), + event('contentBlockStop', { contentBlockIndex: 0 }), + event('contentBlockStart', { + contentBlockIndex: 1, + start: { toolUse: { toolUseId: 'tooluse_1', name: 'get_weather' } }, + }), + event('contentBlockDelta', { contentBlockIndex: 1, delta: { toolUse: { input: '{"city":' } } }), + event('contentBlockDelta', { contentBlockIndex: 1, delta: { toolUse: { input: '"Vienna"}' } } }), + event('contentBlockStop', { contentBlockIndex: 1 }), + event('messageStop', { stopReason: 'tool_use' }), + event('metadata', { usage: { inputTokens: 12, outputTokens: 8, totalTokens: 20 }, metrics: { latencyMs: 100 } }), + ]); + + const response = await createClient().send( + new ConverseStreamCommand({ + modelId, + system: [{ text: 'You are a helpful assistant.' }], + messages: [{ role: 'user', content: [{ text: 'Hello' }] }], + }), + ); + + let text = ''; + for await (const item of response.stream) { + text += item.contentBlockDelta?.delta?.text ?? ''; + } + if (text !== 'Hello from Bedrock') { + throw new Error(`Unexpected streamed text: ${text}`); + } +} + +async function invokeModelWithResponseStream() { + mockStream(/\/model\/.*\/invoke-with-response-stream$/, [ + invokeModelChunk({ + type: 'message_start', + message: { id: 'msg_bdrk_2', model: 'claude-3-5-sonnet-20240620', usage: { input_tokens: 15, output_tokens: 1 } }, + }), + invokeModelChunk({ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }), + invokeModelChunk({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'Streamed ' } }), + invokeModelChunk({ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'reply' } }), + invokeModelChunk({ type: 'content_block_stop', index: 0 }), + invokeModelChunk({ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 9 } }), + invokeModelChunk({ type: 'message_stop' }), + ]); + + const response = await createClient().send( + new InvokeModelWithResponseStreamCommand({ + modelId, + contentType: 'application/json', + body: JSON.stringify({ + anthropic_version: 'bedrock-2023-05-31', + max_tokens: 100, + messages: [{ role: 'user', content: 'Stream please' }], + }), + }), + ); + + let chunks = 0; + for await (const item of response.body) { + if (item.chunk?.bytes) { + chunks++; + } + } + if (chunks !== 7) { + throw new Error(`Unexpected chunk count: ${chunks}`); + } +} + +async function failingConverseStream() { + mockStream(/\/model\/.*\/converse-stream$/, [ + event('messageStart', { role: 'assistant' }), + event('contentBlockDelta', { contentBlockIndex: 0, delta: { text: 'Partial' } }), + exception('modelStreamErrorException', { message: 'The model stream failed', originalStatusCode: 500 }), + ]); + + const response = await createClient().send( + new ConverseStreamCommand({ + modelId: 'amazon.nova-pro-v1:0', + messages: [{ role: 'user', content: [{ text: 'Fail please' }] }], + }), + ); + + try { + for await (const _ of response.stream) { + // drain + } + } catch (error) { + if (error.name !== 'ModelStreamErrorException') { + throw error; + } + return; + } + throw new Error('Expected the stream to fail'); +} + +async function abortedConverseStream() { + mockStream(/\/model\/.*\/converse-stream$/, [ + event('messageStart', { role: 'assistant' }), + event('contentBlockDelta', { contentBlockIndex: 0, delta: { text: 'Partial' } }), + exception('modelStreamErrorException', { message: 'The model stream failed', originalStatusCode: 500 }), + ]); + + const controller = new AbortController(); + const response = await createClient().send( + new ConverseStreamCommand({ + modelId: 'amazon.nova-lite-v1:0', + messages: [{ role: 'user', content: [{ text: 'Stop please' }] }], + }), + { abortSignal: controller.signal }, + ); + + try { + for await (const _ of response.stream) { + controller.abort(); + } + } catch { + return; + } + throw new Error('Expected the stream to fail'); +} + +async function run() { + await Sentry.startSpan({ name: 'Test Transaction' }, async () => { + await converseStream(); + await invokeModelWithResponseStream(); + await failingConverseStream(); + await abortedConverseStream(); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts index 018326729159..24249bf2dcc8 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts @@ -11,6 +11,7 @@ import { SENTRY_OP, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { GEN_AI_CHAT, GEN_AI_GENERATE_CONTENT } from '@sentry/conventions/op'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -34,7 +35,7 @@ function assertBedrockSpans(container: SerializedStreamedSpanContainer): void { expect(converseSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.9); expect(converseSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(12); expect(converseSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(8); - expect(converseSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toEqual(['end_turn']); + expect(converseSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toBe('["end_turn"]'); // InvokeModel (non-streaming, anthropic.claude request/response body) const invokeModelSpan = container.items.find(span => span.name === `generate_content ${MODEL_ID}`); @@ -49,7 +50,30 @@ function assertBedrockSpans(container: SerializedStreamedSpanContainer): void { expect(invokeModelSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.9); expect(invokeModelSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(15); expect(invokeModelSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(9); - expect(invokeModelSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toEqual(['end_turn']); + expect(invokeModelSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toBe('["end_turn"]'); +} + +function assertStreamSpans(container: SerializedStreamedSpanContainer): void { + const converseStreamSpan = container.items.find(span => span.name === `chat ${MODEL_ID}`); + expect(converseStreamSpan!.status).toBe('ok'); + expect(converseStreamSpan!.attributes[SENTRY_OP].value).toBe(GEN_AI_CHAT); + expect(converseStreamSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(12); + expect(converseStreamSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(8); + expect(converseStreamSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toBe('["tool_use"]'); + + const invokeStreamSpan = container.items.find(span => span.name === `generate_content ${MODEL_ID}`); + expect(invokeStreamSpan!.status).toBe('ok'); + expect(invokeStreamSpan!.attributes[SENTRY_OP].value).toBe(GEN_AI_GENERATE_CONTENT); + expect(invokeStreamSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(15); + expect(invokeStreamSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(9); + expect(invokeStreamSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toBe('["end_turn"]'); + + const failedStreamSpan = container.items.find(span => span.name === 'chat amazon.nova-pro-v1:0'); + expect(failedStreamSpan!.status).toBe('error'); + + // `cancelled` is sent as `ok` under span streaming. + const abortedStreamSpan = container.items.find(span => span.name === 'chat amazon.nova-lite-v1:0'); + expect(abortedStreamSpan!.status).toBe('ok'); } describe('awsIntegration - Bedrock', () => { @@ -62,4 +86,10 @@ describe('awsIntegration - Bedrock', () => { await createTestRunner().ignore('event').expect({ span: assertBedrockSpans }).start().completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-stream.mjs', 'instrument.mjs', (createTestRunner, test) => { + test('ends Bedrock stream spans when the streams finish, fail, or are aborted', { timeout: 90_000 }, async () => { + await createTestRunner().ignore('event').expect({ span: assertStreamSpans }).start().completed(); + }); + }); }); diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs index 966fa53fe4eb..05383f2a7a59 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs @@ -1,15 +1,21 @@ import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; import * as fs from 'fs'; +import { readFileSync } from 'fs'; +import { readFile } from 'fs/promises'; import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; +// `__dirname` does not exist in ESM, `import.meta` breaks the CJS variant of this file, and the Bun +// build project runs a bundle outside the test's tmp dir, so the test passes the fixtures dir in. +const fixturesDir = process.env.FIXTURES_DIR; + const app = express(); app.get('/readFile-error', async (_, res) => { try { - await fs.promises.readFile(path.join(__dirname, 'fixtures', 'some-file-that-doesnt-exist.txt'), 'utf-8'); + await fs.promises.readFile(path.join(fixturesDir, 'some-file-that-doesnt-exist.txt'), 'utf-8'); } catch { // noop } @@ -18,59 +24,57 @@ app.get('/readFile-error', async (_, res) => { app.get('/readFile', async (_, res) => { await new Promise(resolve => { - fs.readFile(path.join(__dirname, 'fixtures', 'some-file.txt'), 'utf-8', () => { + fs.readFile(path.join(fixturesDir, 'some-file.txt'), 'utf-8', () => { resolve(); }); }); - await fs.promises.readFile(path.join(__dirname, 'fixtures', 'some-file-promises.txt'), 'utf-8'); - await util.promisify(fs.readFile)(path.join(__dirname, 'fixtures', 'some-file-promisify.txt'), 'utf-8'); + await fs.promises.readFile(path.join(fixturesDir, 'some-file-promises.txt'), 'utf-8'); + await util.promisify(fs.readFile)(path.join(fixturesDir, 'some-file-promisify.txt'), 'utf-8'); + res.send('done'); +}); + +app.get('/named-imports', async (_, res) => { + readFileSync(path.join(fixturesDir, 'some-file.txt'), 'utf-8'); + await readFile(path.join(fixturesDir, 'some-file-promises.txt'), 'utf-8'); res.send('done'); }); app.get('/copyFile', async (_, res) => { await new Promise(resolve => { - fs.copyFile( - path.join(__dirname, 'fixtures', 'some-file.txt'), - path.join(__dirname, 'fixtures', 'some-file.txt.copy'), - () => { - resolve(); - }, - ); + fs.copyFile(path.join(fixturesDir, 'some-file.txt'), path.join(fixturesDir, 'some-file.txt.copy'), () => { + resolve(); + }); }); await fs.promises.copyFile( - path.join(__dirname, 'fixtures', 'some-file-promises.txt'), - path.join(__dirname, 'fixtures', 'some-file-promises.txt.copy'), + path.join(fixturesDir, 'some-file-promises.txt'), + path.join(fixturesDir, 'some-file-promises.txt.copy'), ); await util.promisify(fs.copyFile)( - path.join(__dirname, 'fixtures', 'some-file-promisify.txt'), - path.join(__dirname, 'fixtures', 'some-file-promisify.txt.copy'), + path.join(fixturesDir, 'some-file-promisify.txt'), + path.join(fixturesDir, 'some-file-promisify.txt.copy'), ); res.send('done'); }); app.get('/link', async (_, res) => { await new Promise(resolve => { - fs.link( - path.join(__dirname, 'fixtures', 'some-file.txt'), - path.join(__dirname, 'fixtures', 'some-file.txt.link'), - () => { - resolve(); - }, - ); + fs.link(path.join(fixturesDir, 'some-file.txt'), path.join(fixturesDir, 'some-file.txt.link'), () => { + resolve(); + }); }); await fs.promises.link( - path.join(__dirname, 'fixtures', 'some-file-promises.txt'), - path.join(__dirname, 'fixtures', 'some-file-promises.txt.link'), + path.join(fixturesDir, 'some-file-promises.txt'), + path.join(fixturesDir, 'some-file-promises.txt.link'), ); await util.promisify(fs.link)( - path.join(__dirname, 'fixtures', 'some-file-promisify.txt'), - path.join(__dirname, 'fixtures', 'some-file-promisify.txt.link'), + path.join(fixturesDir, 'some-file-promisify.txt'), + path.join(fixturesDir, 'some-file-promisify.txt.link'), ); await Promise.all([ - fs.promises.unlink(path.join(__dirname, 'fixtures', 'some-file.txt.link')), - fs.promises.unlink(path.join(__dirname, 'fixtures', 'some-file-promises.txt.link')), - fs.promises.unlink(path.join(__dirname, 'fixtures', 'some-file-promisify.txt.link')), + fs.promises.unlink(path.join(fixturesDir, 'some-file.txt.link')), + fs.promises.unlink(path.join(fixturesDir, 'some-file-promises.txt.link')), + fs.promises.unlink(path.join(fixturesDir, 'some-file-promisify.txt.link')), ]); res.send('done'); @@ -90,37 +94,33 @@ app.get('/mkdtemp', async (_, res) => { app.get('/exists', async (_, res) => { await new Promise(resolve => { - fs.exists(path.join(__dirname, 'fixtures', 'some-file.txt'), () => { + fs.exists(path.join(fixturesDir, 'some-file.txt'), () => { resolve(); }); }); - await util.promisify(fs.exists)(path.join(__dirname, 'fixtures', 'some-file-promisify.txt')); + await util.promisify(fs.exists)(path.join(fixturesDir, 'some-file-promisify.txt')); res.send('done'); }); app.get('/symlink', async (_, res) => { await new Promise(resolve => { - fs.symlink( - path.join(__dirname, 'fixtures', 'some-file.txt'), - path.join(__dirname, 'fixtures', 'some-file.txt.symlink'), - () => { - resolve(); - }, - ); + fs.symlink(path.join(fixturesDir, 'some-file.txt'), path.join(fixturesDir, 'some-file.txt.symlink'), () => { + resolve(); + }); }); await fs.promises.symlink( - path.join(__dirname, 'fixtures', 'some-file-promises.txt'), - path.join(__dirname, 'fixtures', 'some-file-promises.txt.symlink'), + path.join(fixturesDir, 'some-file-promises.txt'), + path.join(fixturesDir, 'some-file-promises.txt.symlink'), ); await util.promisify(fs.symlink)( - path.join(__dirname, 'fixtures', 'some-file-promisify.txt'), - path.join(__dirname, 'fixtures', 'some-file-promisify.txt.symlink'), + path.join(fixturesDir, 'some-file-promisify.txt'), + path.join(fixturesDir, 'some-file-promisify.txt.symlink'), ); await Promise.all([ - fs.promises.unlink(path.join(__dirname, 'fixtures', 'some-file.txt.symlink')), - fs.promises.unlink(path.join(__dirname, 'fixtures', 'some-file-promises.txt.symlink')), - fs.promises.unlink(path.join(__dirname, 'fixtures', 'some-file-promisify.txt.symlink')), + fs.promises.unlink(path.join(fixturesDir, 'some-file.txt.symlink')), + fs.promises.unlink(path.join(fixturesDir, 'some-file-promises.txt.symlink')), + fs.promises.unlink(path.join(fixturesDir, 'some-file-promisify.txt.symlink')), ]); res.send('done'); diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts index e4d4a1474e05..81b366889d37 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts @@ -1,7 +1,9 @@ import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { join } from 'path'; import { afterAll, describe, expect } from 'vitest'; -import { cleanupChildProcesses, createCjsTests } from '../../utils/runner'; +import { supports } from '../../utils'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../utils/runner'; type FileSpan = Pick; @@ -37,11 +39,16 @@ describe('fs instrumentation', () => { }); describe('records file paths and error messages', () => { - createCjsTests( + createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', - (createRunner, test) => { + (createTestRunner, test, mode, cwd) => { + const createRunner = (): ReturnType => + createTestRunner().withEnv({ FIXTURES_DIR: join(cwd, 'fixtures') }); + // On Bun and Deno the ESM `node:fs` exports are not tied to the `fs` object we patch, so only `fs.promises` is traced there. + const testUnlessNonNodeEsm = test.runIf(mode === 'cjs' || supports({ runtimes: ['node'] })); + test('should create spans for fs operations that take target argument', async () => { const runner = createRunner() .expect({ @@ -68,7 +75,7 @@ describe('fs instrumentation', () => { await runner.completed(); }); - test('should create spans for fs operations that take one path', async () => { + testUnlessNonNodeEsm('should create spans for fs operations that take one path', async () => { const runner = createRunner() .expect({ span: expectFileSpans('GET /readFile', [ @@ -111,7 +118,45 @@ describe('fs instrumentation', () => { await runner.completed(); }); - test('should create spans for fs operations that take src and dest arguments', async () => { + // CJS destructuring copies the functions at require time, before the instrumentation patches them. + test.runIf(mode === 'esm' && supports({ runtimes: ['node'] }))( + 'should create spans for named imports from fs and fs/promises', + async () => { + const runner = createRunner() + .expect({ + span: expectFileSpans('GET /named-imports', [ + { + name: 'fs.readFileSync', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promises.txt'), + }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), + }) + .start(); + + const result = await runner.makeRequest('get', '/named-imports'); + expect(result).toEqual('done'); + await runner.completed(); + }, + ); + + testUnlessNonNodeEsm('should create spans for fs operations that take src and dest arguments', async () => { const runner = createRunner() .expect({ span: expectFileSpans('GET /copyFile', [ @@ -160,54 +205,69 @@ describe('fs instrumentation', () => { await runner.completed(); }); - test('should create spans for fs operations that take existing path and new path arguments', async () => { - const runner = createRunner() - .expect({ - span: expectFileSpans('GET /link', [ - { - name: 'fs.link', - status: 'ok', - attributes: { - existing_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, - new_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.link') }, - [SENTRY_OP]: { type: 'string', value: 'file' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, - }, - }, - { - name: 'fs.link', - status: 'ok', - attributes: { - existing_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt') }, - new_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt.link') }, - [SENTRY_OP]: { type: 'string', value: 'file' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + testUnlessNonNodeEsm( + 'should create spans for fs operations that take existing path and new path arguments', + async () => { + const runner = createRunner() + .expect({ + span: expectFileSpans('GET /link', [ + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file.txt'), + }, + new_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file.txt.link'), + }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, }, - }, - { - name: 'fs.link', - status: 'ok', - attributes: { - existing_path_argument: { - type: 'string', - value: expect.stringMatching('/fixtures/some-file-promisify.txt'), + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { + type: 'string', + value: expect.stringMatching('/some-file-promises.txt'), + }, + new_path_argument: { + type: 'string', + value: expect.stringMatching('/some-file-promises.txt.link'), + }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, - new_path_argument: { - type: 'string', - value: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), + }, + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), + }, + new_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), + }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, - [SENTRY_OP]: { type: 'string', value: 'file' }, - [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, - }, - ]), - }) - .start(); + ]), + }) + .start(); - const result = await runner.makeRequest('get', '/link'); - expect(result).toEqual('done'); - await runner.completed(); - }); + const result = await runner.makeRequest('get', '/link'); + expect(result).toEqual('done'); + await runner.completed(); + }, + ); test('should create spans for fs operations that take prefix argument', async () => { const runner = createRunner() @@ -249,7 +309,7 @@ describe('fs instrumentation', () => { await runner.completed(); }); - test('should create spans for fs symlink operations that take target argument', async () => { + testUnlessNonNodeEsm('should create spans for fs symlink operations that take target argument', async () => { const runner = createRunner() .expect({ span: expectFileSpans('GET /symlink', [ @@ -304,7 +364,7 @@ describe('fs instrumentation', () => { await runner.completed(); }); - test('should create spans for fs.exists callback and promisified versions', async () => { + testUnlessNonNodeEsm('should create spans for fs.exists callback and promisified versions', async () => { const runner = createRunner() .expect({ span: expectFileSpans('GET /exists', [ @@ -343,11 +403,13 @@ describe('fs instrumentation', () => { }); describe('records file paths only', () => { - createCjsTests( + createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument-record-paths-only.mjs', - (createRunner, test) => { + (createTestRunner, test, _mode, cwd) => { + const createRunner = (): ReturnType => + createTestRunner().withEnv({ FIXTURES_DIR: join(cwd, 'fixtures') }); test('records file path but not error messages when only `recordFilePaths` is enabled', async () => { const runner = createRunner() .expect({ @@ -379,11 +441,13 @@ describe('fs instrumentation', () => { }); describe('records error messages only', () => { - createCjsTests( + createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument-record-errors-only.mjs', - (createRunner, test) => { + (createTestRunner, test, _mode, cwd) => { + const createRunner = (): ReturnType => + createTestRunner().withEnv({ FIXTURES_DIR: join(cwd, 'fixtures') }); test('records error messages but not file paths when only `recordErrorMessagesAsSpanAttributes` is enabled', async () => { const runner = createRunner() .expect({ diff --git a/dev-packages/node-integration-tests/suites/hono-sdk/scenario.mjs b/dev-packages/node-integration-tests/suites/hono-sdk/scenario.mjs index d27dc20bbc30..c8ec88a9975b 100644 --- a/dev-packages/node-integration-tests/suites/hono-sdk/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/hono-sdk/scenario.mjs @@ -1,5 +1,5 @@ import { serve } from '@hono/node-server'; -import { sentry } from '@sentry/hono/node'; +import { captureMessage, sentry } from '@sentry/hono/node'; import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; import { Hono } from 'hono'; @@ -20,6 +20,61 @@ app.get('/error/:param', () => { throw new Error('Test error from Hono app'); }); +// Both routes match `/users/me`. The name must come from the handler that responded. +app.get('/users/me', c => c.text('me')); +app.get('/users/:id', c => c.text(`user ${c.req.param('id')}`)); + +// The catch-all below also matches. An event from inside this handler must still get this route. +app.get('/users/:id/capture', c => { + captureMessage('Message from a route handler'); + return c.text('captured'); +}); + +// A middleware that returns early. The request gets the route that would have run. +app.use('/early/*', async (c, _next) => c.text('blocked by middleware')); +app.get('/early/:id', c => c.text('never reached')); + +// Only a middleware matches this path. +app.use('/mw-only/*', async (_c, _next) => new Response('handled by middleware')); + +const subApp = new Hono(); +subApp.get('/hello', c => c.text('sub hello')); +app.route('/sub', subApp); + +app.basePath('/base').get('/hello', c => c.text('base hello')); + +app.all('/any-method', c => c.text(`any method: ${c.req.method}`)); + +// Hono answers `HEAD` with the `GET` route. The SDK records no server span for `HEAD`, so the test +// checks the name on the error event. +app.get('/head-error/:id', () => { + throw new Error('Test error from a HEAD request'); +}); + +const innerApp = new Hono(); +innerApp.get('/item/:itemId', c => c.json({ itemId: c.req.param('itemId') })); +app.get('/outer/:itemId', async c => { + const res = await innerApp.request(`/item/${c.req.param('itemId')}`); + return c.json({ outer: c.req.param('itemId'), inner: await res.json() }); +}); + +// Passes `draft` on to the catch-all below, which then names the request. +app.get('/posts/:id', async (c, next) => { + if (c.req.param('id') === 'draft') { + await next(); + return; + } + return c.text('post'); +}); +app.get('/posts/*', c => { + captureMessage('Message from a route handler after next()'); + return c.text('fallback'); +}); + +// A catch-all registered last. It also matches `/users/me` and `/users/42`, but must not name them. +app.get('/users/*', c => c.text('users catch-all')); +app.get('/early/*', c => c.text('never reached')); + serve({ fetch: app.fetch, port: 0 }, info => { sendPortToRunner(info.port); }); diff --git a/dev-packages/node-integration-tests/suites/hono-sdk/test.ts b/dev-packages/node-integration-tests/suites/hono-sdk/test.ts index 1366be964a95..ca34ff88baf8 100644 --- a/dev-packages/node-integration-tests/suites/hono-sdk/test.ts +++ b/dev-packages/node-integration-tests/suites/hono-sdk/test.ts @@ -1,12 +1,97 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../utils/runner'; +const ROUTE_NAME_CASES: Array<{ method: 'get' | 'post'; path: string; name: string; route: string }> = [ + { method: 'get', path: '/users/me', name: 'GET /users/me', route: '/users/me' }, + { method: 'get', path: '/users/42', name: 'GET /users/:id', route: '/users/:id' }, + { method: 'get', path: '/users/42/posts', name: 'GET /users/*', route: '/users/*' }, + { method: 'get', path: '/early/1', name: 'GET /early/:id', route: '/early/:id' }, + { method: 'get', path: '/posts/1', name: 'GET /posts/:id', route: '/posts/:id' }, + { method: 'get', path: '/posts/draft', name: 'GET /posts/*', route: '/posts/*' }, + { method: 'get', path: '/mw-only/anything', name: 'GET /mw-only/*', route: '/mw-only/*' }, + { method: 'get', path: '/sub/hello', name: 'GET /sub/hello', route: '/sub/hello' }, + { method: 'get', path: '/base/hello', name: 'GET /base/hello', route: '/base/hello' }, + { method: 'post', path: '/any-method', name: 'POST /any-method', route: '/any-method' }, + { method: 'get', path: '/outer/self-watering-plant', name: 'GET /outer/:itemId', route: '/outer/:itemId' }, +]; + describe('hono-sdk (Node)', () => { afterAll(() => { cleanupChildProcesses(); }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + // All cases in one test, because each `test()` starts its own Node process. + test('names each request after the route of the handler that responded', async () => { + const runner = createRunner().unordered(); + for (const { name, route } of ROUTE_NAME_CASES) { + runner.expect({ + span: container => { + const segment = container.items.find(item => item.is_segment && item.name === name); + if (!segment) { + throw new Error(`segment for \`${name}\` not in this container`); + } + expect(segment.attributes?.['http.route']).toEqual({ type: 'string', value: route }); + expect(segment.attributes?.['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); + }, + }); + } + + const started = runner.start(); + for (const { method, path } of ROUTE_NAME_CASES) { + await started.makeRequest(method, path); + } + await started.completed(); + }); + + test('names an event captured in a route handler after that route', async () => { + const runner = createRunner() + .ignore('span') + .expect({ event: { message: 'Message from a route handler', transaction: 'GET /users/:id/capture' } }) + .start(); + runner.makeRequest('get', '/users/42/capture'); + await runner.completed(); + }); + + test('keeps the runtime name for a request without a route', async () => { + const runner = createRunner() + .expect({ + span: container => { + const segment = container.items.find(item => item.is_segment); + expect(segment?.name).toBe('GET'); + expect(segment?.attributes?.['sentry.segment.name.source']).toEqual({ type: 'string', value: 'url' }); + expect(segment?.attributes?.['url.path']).toEqual({ type: 'string', value: '/does-not-exist' }); + expect(segment?.attributes?.['http.route']).toBeUndefined(); + }, + }) + .start(); + runner.makeRequest('get', '/does-not-exist', { expectError: true }); + await runner.completed(); + }); + + test('names an event from the route handler that runs after next() after that route', async () => { + const runner = createRunner() + .ignore('span') + .expect({ event: { message: 'Message from a route handler after next()', transaction: 'GET /posts/*' } }) + .start(); + runner.makeRequest('get', '/posts/draft'); + await runner.completed(); + }); + + test('names a HEAD request after the GET route', async () => { + const runner = createRunner() + .ignore('span') + .expect({ + event: { + exception: { values: [{ value: 'Test error from a HEAD request' }] }, + transaction: 'HEAD /head-error/:id', + }, + }) + .start(); + runner.makeRequest('head', '/head-error/1', { expectError: true }); + await runner.completed(); + }); + test('creates a segment span for a basic GET request', async () => { const runner = createRunner() .expect({ diff --git a/dev-packages/node-integration-tests/suites/hono/scenario-honoMiddleware.mjs b/dev-packages/node-integration-tests/suites/hono/scenario-honoMiddleware.mjs new file mode 100644 index 000000000000..fa4035188210 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/hono/scenario-honoMiddleware.mjs @@ -0,0 +1,38 @@ +import { serve } from '@hono/node-server'; +import * as Sentry from '@sentry/node'; +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import { Hono } from 'hono'; + +// The auto-instrumentation and the manual `honoMiddleware(app)` are both active. +const app = new Hono(); +app.use(Sentry.honoMiddleware(app)); + +// Counts the Sentry wrappers around the handler that runs. Both setups wrap, and the router reuses the +// handler list for each request. +function wrapperCount(c) { + const [matchResultKey] = Object.getOwnPropertySymbols(Object.getPrototypeOf(c.req)); + let handler = c.req[matchResultKey][0][c.req.routeIndex][0][0]; + let count = 0; + while (handler.__sentry_original__) { + handler = handler.__sentry_original__; + count++; + } + return count; +} + +// Only one route handler matches, so the SDK does not wrap. +app.get('/users/me', c => c.json({ wrappers: wrapperCount(c) })); + +// Two route handlers match, so the SDK wraps them. +app.get('/posts/:id', async (c, next) => { + if (c.req.param('id') === 'draft') { + await next(); + return; + } + return c.json({ wrappers: wrapperCount(c) }); +}); +app.get('/posts/*', c => c.json({ wrappers: wrapperCount(c) })); + +serve({ fetch: app.fetch, port: 0 }, info => { + sendPortToRunner(info.port); +}); diff --git a/dev-packages/node-integration-tests/suites/hono/scenario.mjs b/dev-packages/node-integration-tests/suites/hono/scenario.mjs index ed0f816c03cf..0f0eef09b9ce 100644 --- a/dev-packages/node-integration-tests/suites/hono/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/hono/scenario.mjs @@ -1,5 +1,6 @@ import { serve } from '@hono/node-server'; import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import { captureMessage, startSpan } from '@sentry/node'; import { Hono } from 'hono'; // No `@sentry/hono` and no `sentry()` middleware: the app is instrumented automatically by the @@ -97,6 +98,55 @@ app }) .get('/hello', c => c.json({ greeting: 'world' })); +// Both routes match `/users/me`. The name must come from the handler that responded. +app.get('/users/me', c => c.text('me')); +app.get('/users/:id', c => c.text(`user ${c.req.param('id')}`)); + +// The catch-all below also matches. An event from inside this handler must still get this route. +app.get('/users/:id/capture', c => { + captureMessage('Message from a route handler'); + return c.text('captured'); +}); + +// A middleware that returns early. The request gets the route that would have run. +app.use('/early/*', async (c, _next) => c.text('blocked by middleware')); +app.get('/early/:id', c => c.text('never reached')); + +app.all('/any-method', c => c.text(`any method: ${c.req.method}`)); + +// Hono answers `HEAD` with the `GET` route. The SDK records no server span for `HEAD`, so the test +// checks the name on the error event. +app.get('/head-error/:id', () => { + throw new Error('Test error from a HEAD request'); +}); + +// A user span around the route handler. The SDK must rename the segment, not this span. Two route +// handlers match, so the SDK renames inside this span. +app.use('/traced/*', (_c, next) => startSpan({ name: 'user span' }, () => next())); +app.get('/traced/:id', c => c.text('traced')); +app.get('/traced/*', c => c.text('traced catch-all')); + +// Arity does not count rest parameters, so this middleware looks like a route handler. +app.use('/rest-args/*', (_c, ...rest) => rest[0]()); +app.get('/rest-args/:id', c => c.text('rest args')); + +// Passes `draft` on to the catch-all below, which then names the request. +app.get('/posts/:id', async (c, next) => { + if (c.req.param('id') === 'draft') { + await next(); + return; + } + return c.text('post'); +}); +app.get('/posts/*', c => { + captureMessage('Message from a route handler after next()'); + return c.text('fallback'); +}); + +// A catch-all registered last. It also matches `/users/me` and `/users/42`, but must not name them. +app.get('/users/*', c => c.text('users catch-all')); +app.get('/early/*', c => c.text('never reached')); + serve({ fetch: app.fetch, port: 0 }, info => { sendPortToRunner(info.port); }); diff --git a/dev-packages/node-integration-tests/suites/hono/test.ts b/dev-packages/node-integration-tests/suites/hono/test.ts index 20a16f7e1eba..d836673a101e 100644 --- a/dev-packages/node-integration-tests/suites/hono/test.ts +++ b/dev-packages/node-integration-tests/suites/hono/test.ts @@ -15,12 +15,131 @@ type StreamedSpan = { name?: string; status?: string; is_segment?: boolean; attr const attr = (span: StreamedSpan, key: string): unknown => span.attributes?.[key]?.value; const op = (span: StreamedSpan): unknown => attr(span, 'sentry.op'); +const ROUTE_NAME_CASES: Array<{ method: 'get' | 'post'; path: string; name: string; route: string }> = [ + { method: 'get', path: '/users/me', name: 'GET /users/me', route: '/users/me' }, + { method: 'get', path: '/users/42', name: 'GET /users/:id', route: '/users/:id' }, + { method: 'get', path: '/users/42/posts', name: 'GET /users/*', route: '/users/*' }, + { method: 'get', path: '/overlap/123', name: 'GET /overlap/:id', route: '/overlap/:id' }, + { method: 'get', path: '/early/1', name: 'GET /early/:id', route: '/early/:id' }, + { method: 'get', path: '/posts/1', name: 'GET /posts/:id', route: '/posts/:id' }, + { method: 'get', path: '/posts/draft', name: 'GET /posts/*', route: '/posts/*' }, + { method: 'get', path: '/mw-only/anything', name: 'GET /mw-only/*', route: '/mw-only/*' }, + { method: 'get', path: '/rest-args/1', name: 'GET /rest-args/:id', route: '/rest-args/:id' }, + { method: 'get', path: '/sub/hello', name: 'GET /sub/hello', route: '/sub/hello' }, + { + method: 'get', + path: '/test-basepath-mw/hello', + name: 'GET /test-basepath-mw/hello', + route: '/test-basepath-mw/hello', + }, + { method: 'post', path: '/any-method', name: 'POST /any-method', route: '/any-method' }, + { method: 'get', path: '/outer/self-watering-plant', name: 'GET /outer/:itemId', route: '/outer/:itemId' }, +]; + +function segmentNamed(name: string, route: string): { span: (container: { items: StreamedSpan[] }) => void } { + return { + span: container => { + const segment = container.items.find(item => item.is_segment && item.name === name); + if (!segment) { + throw new Error(`segment for \`${name}\` not in this container`); + } + expect(op(segment)).toBe('http.server'); + expect(attr(segment, 'http.route')).toBe(route); + expect(attr(segment, 'sentry.segment.name.source')).toBe('route'); + }, + }; +} + describe('hono auto-instrumentation', () => { afterAll(() => { cleanupChildProcesses(); }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + // All cases in one test, because each `test()` starts its own Node process. + test('names each request after the route of the handler that responded', async () => { + const runner = createRunner().unordered(); + for (const { name, route } of ROUTE_NAME_CASES) { + runner.expect(segmentNamed(name, route)); + } + + const started = runner.start(); + for (const { method, path } of ROUTE_NAME_CASES) { + await started.makeRequest(method, path); + } + await started.completed(); + }); + + test('names an event captured in a route handler after that route', async () => { + const runner = createRunner() + .ignore('span') + .expect({ event: { message: 'Message from a route handler', transaction: 'GET /users/:id/capture' } }) + .start(); + runner.makeRequest('get', '/users/42/capture'); + await runner.completed(); + }); + + test('does not rename a span that a middleware starts around the route handler', async () => { + const runner = createRunner() + .unordered() + .expect({ + span: container => { + const segment = container.items.find(item => item.is_segment && item.name === 'GET /traced/:id'); + if (!segment) { + throw new Error('segment for `GET /traced/:id` not in this container'); + } + expect(container.items.filter(item => item.name === 'user span')).toHaveLength(1); + }, + }) + .start(); + runner.makeRequest('get', '/traced/1'); + await runner.completed(); + }); + + test('keeps the runtime name for a request without a route', async () => { + const runner = createRunner() + .unordered() + .expect({ + span: container => { + const segment = container.items.find( + item => item.is_segment && attr(item, 'url.path') === '/does-not-exist', + ); + if (!segment) { + throw new Error('segment for `/does-not-exist` not in this container'); + } + expect(segment.name).toBe('GET'); + expect(attr(segment, 'sentry.segment.name.source')).toBe('url'); + expect(attr(segment, 'http.route')).toBeUndefined(); + }, + }) + .start(); + runner.makeRequest('get', '/does-not-exist', { expectError: true }); + await runner.completed(); + }); + + test('names an event from the route handler that runs after next() after that route', async () => { + const runner = createRunner() + .ignore('span') + .expect({ event: { message: 'Message from a route handler after next()', transaction: 'GET /posts/*' } }) + .start(); + runner.makeRequest('get', '/posts/draft'); + await runner.completed(); + }); + + test('names a HEAD request after the GET route', async () => { + const runner = createRunner() + .unordered() + .expect({ + event: { + exception: { values: [{ value: 'Test error from a HEAD request' }] }, + transaction: 'HEAD /head-error/:id', + }, + }) + .start(); + runner.makeRequest('head', '/head-error/1', { expectError: true }); + await runner.completed(); + }); + test('creates a transaction for a basic GET request', async () => { const runner = createRunner() .unordered() @@ -227,42 +346,6 @@ describe('hono auto-instrumentation', () => { await runner.completed(); }); - test('resolves overlapping handlers to the responding route, not the catch-all', async () => { - const runner = createRunner() - .unordered() - .expect({ - span: container => { - const segment = container.items.find(item => item.is_segment && item.name === 'GET /overlap/:id'); - if (!segment) { - throw new Error('segment for `GET /overlap/:id` not in this container'); - } - // The catch-all `GET /overlap/*` handler also matched, but the parameterized handler - // responded — the route name must not collapse to `/overlap/*`. - expect(attr(segment, 'sentry.segment.name.source')).toBe('route'); - }, - }) - .start(); - runner.makeRequest('get', '/overlap/123'); - await runner.completed(); - }); - - test('falls back to the matched middleware path when only middleware matched', async () => { - const runner = createRunner() - .unordered() - .expect({ - span: container => { - const segment = container.items.find(item => item.is_segment && item.name === 'GET /mw-only/*'); - if (!segment) { - throw new Error('segment for `GET /mw-only/*` not in this container'); - } - expect(op(segment)).toBe('http.server'); - }, - }) - .start(); - runner.makeRequest('get', '/mw-only/anything'); - await runner.completed(); - }); - test('does not set the middleware span to error for a 3xx middleware error', async () => { const runner = createRunner() .unordered() @@ -321,3 +404,28 @@ describe('hono auto-instrumentation', () => { }); }); }); + +describe('hono auto-instrumentation with honoMiddleware(app)', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario-honoMiddleware.mjs', 'instrument.mjs', (createRunner, test) => { + test('wraps route handlers only when more than one matches, and only once', async () => { + const runner = createRunner() + .unordered() + .expect(segmentNamed('GET /users/me', '/users/me')) + .expect(segmentNamed('GET /posts/:id', '/posts/:id')) + .expect(segmentNamed('GET /posts/:id', '/posts/:id')) + .expect(segmentNamed('GET /posts/*', '/posts/*')) + .start(); + + expect(await runner.makeRequest('get', '/users/me')).toEqual({ wrappers: 0 }); + // The second request gets the same cached handler list. + expect(await runner.makeRequest('get', '/posts/1')).toEqual({ wrappers: 1 }); + expect(await runner.makeRequest('get', '/posts/1')).toEqual({ wrappers: 1 }); + expect(await runner.makeRequest('get', '/posts/draft')).toEqual({ wrappers: 1 }); + await runner.completed(); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 144d1e16a827..8abd3e0c088e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -452,18 +452,23 @@ describe('Anthropic integration', () => { expect(streamInitErrorSpan!.status).toBe('error'); const createMidwayErrorSpan = spans.find( - span => span.attributes[GEN_AI_REQUEST_MODEL]?.value === 'error-stream-midway' && span.status === 'ok', + span => + span.attributes[GEN_AI_REQUEST_MODEL]?.value === 'error-stream-midway' && + span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, ); expect(createMidwayErrorSpan!.name).toBe('chat error-stream-midway'); - expect(createMidwayErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(createMidwayErrorSpan!.status).toBe('error'); expect(createMidwayErrorSpan!.attributes[GEN_AI_RESPONSE_STREAMING].value).toBe(true); expect(createMidwayErrorSpan!.attributes[GEN_AI_RESPONSE_TEXT].value).toBe('This stream will '); const streamMidwayErrorSpan = spans.find( - span => span.attributes[GEN_AI_REQUEST_MODEL]?.value === 'error-stream-midway' && span.status === 'error', + span => + span.attributes[GEN_AI_REQUEST_MODEL]?.value === 'error-stream-midway' && + span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, ); expect(streamMidwayErrorSpan!.name).toBe('chat error-stream-midway'); - expect(streamMidwayErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(streamMidwayErrorSpan!.status).toBe('error'); + expect(streamMidwayErrorSpan!.attributes[GEN_AI_RESPONSE_TEXT].value).toBe('This stream will '); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/scenario-environment-worker.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/scenario-environment-worker.mjs new file mode 100644 index 000000000000..44184f38bf03 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/scenario-environment-worker.mjs @@ -0,0 +1,108 @@ +import Anthropic from '@anthropic-ai/sdk'; +import * as Sentry from '@sentry/node'; +import express from 'express'; +import { tmpdir } from 'node:os'; + +const WORK_ITEMS = [ + { id: 'work_vienna', type: 'work', environment_id: 'env_1', data: { type: 'session', id: 'sesn_vienna' } }, + { id: 'work_graz', type: 'work', environment_id: 'env_1', data: { type: 'session', id: 'sesn_graz' } }, +]; + +const STREAM_EVENTS = { + sesn_vienna: [ + { type: 'agent.tool_use', id: 'sevt_vienna', name: 'weather', input: { city: 'Vienna' } }, + { type: 'session.status_terminated', id: 'sevt_vienna_end' }, + ], + sesn_graz: [ + { type: 'agent.tool_use', id: 'sevt_graz', name: 'weather', input: { city: 'Graz' } }, + { type: 'session.status_terminated', id: 'sevt_graz_end' }, + ], +}; + +function startMockServer(onAllStopped) { + const app = express(); + app.use(express.json()); + + const queue = [...WORK_ITEMS]; + let stopped = 0; + + app.get('/anthropic/v1/environments/:environmentId/work/poll', (_req, res) => { + res.json(queue.shift() ?? null); + }); + app.post('/anthropic/v1/environments/:environmentId/work/:workId/ack', (req, res) => { + res.json(WORK_ITEMS.find(work => work.id === req.params.workId)); + }); + app.post('/anthropic/v1/environments/:environmentId/work/:workId/heartbeat', (_req, res) => { + res.json({ lease_extended: true, state: 'active', ttl_seconds: 30 }); + }); + app.post('/anthropic/v1/environments/:environmentId/work/:workId/stop', (req, res) => { + res.json(WORK_ITEMS.find(work => work.id === req.params.workId)); + stopped++; + if (stopped === WORK_ITEMS.length) { + onAllStopped(); + } + }); + + app.get('/anthropic/v1/sessions/:sessionId', (req, res) => { + res.json({ id: req.params.sessionId, type: 'session', resources: [] }); + }); + app.get('/anthropic/v1/sessions/:sessionId/events/stream', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }); + + const events = STREAM_EVENTS[req.params.sessionId]; + events.forEach((event, index) => { + setTimeout(() => { + res.write(`event: ${event.type}\n`); + res.write(`data: ${JSON.stringify(event)}\n\n`); + if (index === events.length - 1) { + res.end(); + } + }, index * 10); + }); + }); + app.get('/anthropic/v1/sessions/:sessionId/events', (_req, res) => { + res.json({ data: [], next_page: null }); + }); + app.post('/anthropic/v1/sessions/:sessionId/events', (_req, res) => { + res.json({ data: [] }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const controller = new AbortController(); + const server = await startMockServer(() => controller.abort()); + + const client = new Anthropic({ + apiKey: 'mock-api-key', + baseURL: `http://localhost:${server.address().port}/anthropic`, + }); + const worker = client.beta.environments.work.worker({ + environmentId: 'env_1', + environmentKey: 'mock-environment-key', + workdir: tmpdir(), + memorySyncIntervalMs: null, + tools: () => [ + { + name: 'weather', + description: 'Get the weather by city', + input_schema: { type: 'object', properties: { city: { type: 'string' } } }, + run: () => 'sunny', + }, + ], + }); + + // Outside any span, as the SDK docs run the worker. + await worker.run(controller.signal); + + await Sentry.flush(2000); + + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/test.ts index 3c66fe8cb531..77d6b64770c1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/test.ts @@ -1,11 +1,12 @@ import type { Event, SerializedStreamedSpanContainer } from '@sentry/core'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_RESPONSE_ID, GEN_AI_RESPONSE_STREAMING, GEN_AI_TOOL_NAME, SENTRY_OP, } from '@sentry/conventions/attributes'; -import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; +import { GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -138,12 +139,54 @@ describe('Anthropic integration (0.129)', () => { }, ); + createEsmAndCjsTests( + __dirname, + 'scenario-environment-worker.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('runs each session of an EnvironmentWorker in a trace of its own', async () => { + const receivedSpans: SerializedStreamedSpanContainer['items'] = []; + const sessionSegment = { + span: (container: SerializedStreamedSpanContainer) => { + expect(container.items.find(span => span.is_segment && span.name === 'invoke_agent')).toBeDefined(); + receivedSpans.push(...container.items); + }, + }; + + await createRunner().unordered().expect(sessionSegment).expect(sessionSegment).start().completed(); + + const agentSpans = receivedSpans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT); + expect(agentSpans).toHaveLength(2); + expect(agentSpans[0]!.trace_id).not.toBe(agentSpans[1]!.trace_id); + + for (const sessionId of ['sesn_vienna', 'sesn_graz']) { + const agent = agentSpans.find(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === sessionId)!; + expect(agent).toMatchObject({ name: 'invoke_agent', is_segment: true, status: 'ok' }); + expect(agent.parent_span_id).toBeUndefined(); + + const toolSpans = receivedSpans.filter( + span => + span.attributes[SENTRY_OP]?.value === GEN_AI_EXECUTE_TOOL && + span.attributes[GEN_AI_CONVERSATION_ID]?.value === sessionId, + ); + expect(toolSpans).toHaveLength(1); + expect(toolSpans[0]).toMatchObject({ name: 'execute_tool weather', parent_span_id: agent.span_id }); + } + }); + }, + { + additionalDependencies: { + '@anthropic-ai/sdk': '0.129.0', + }, + }, + ); + createEsmAndCjsTests( __dirname, 'scenario-session-tool-runner.mjs', 'instrument.mjs', (createRunner, test) => { - test('emits an execute_tool span for each tool a session runner runs, however it was created', async () => { + test('parents the execute_tool spans of each session runner to one invoke_agent span', async () => { let receivedEvent: Event | undefined; let receivedSpans: SerializedStreamedSpanContainer['items'] = []; @@ -164,6 +207,17 @@ describe('Anthropic integration (0.129)', () => { .completed(); const main = receivedSpans.find(span => span.is_segment && span.name === 'main')!; + const agentSpans = receivedSpans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT); + expect(agentSpans).toHaveLength(2); + const weatherSession = agentSpans.find( + span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'sesn_weather', + )!; + const directSession = agentSpans.find( + span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === 'sesn_direct', + )!; + expect(weatherSession).toMatchObject({ name: 'invoke_agent', parent_span_id: main.span_id, status: 'ok' }); + expect(directSession).toMatchObject({ name: 'invoke_agent', parent_span_id: main.span_id, status: 'ok' }); + const toolSpans = receivedSpans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_EXECUTE_TOOL); expect(toolSpans).toHaveLength(3); const weather = toolSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'weather')!; @@ -172,19 +226,25 @@ describe('Anthropic integration (0.129)', () => { expect(weather).toMatchObject({ name: 'execute_tool weather', - parent_span_id: main.span_id, + parent_span_id: weatherSession.span_id, status: 'ok', attributes: expect.objectContaining({ [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: { type: 'string', value: 'sevt_weather' }, + [GEN_AI_CONVERSATION_ID]: { type: 'string', value: 'sesn_weather' }, }), }); - expect(explode).toMatchObject({ name: 'execute_tool explode', parent_span_id: main.span_id, status: 'error' }); + expect(explode).toMatchObject({ + name: 'execute_tool explode', + parent_span_id: weatherSession.span_id, + status: 'error', + }); expect(read).toMatchObject({ name: 'execute_tool read', - parent_span_id: main.span_id, + parent_span_id: directSession.span_id, status: 'ok', attributes: expect.objectContaining({ [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: { type: 'string', value: 'sevt_read' }, + [GEN_AI_CONVERSATION_ID]: { type: 'string', value: 'sesn_direct' }, }), }); expect(receivedEvent).toMatchObject({ diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/v1/test.ts index 0983283d178f..48cf3bd9bde0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/v1/test.ts @@ -1,12 +1,13 @@ import type { Event, SerializedStreamedSpanContainer } from '@sentry/core'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_RESPONSE_ID, GEN_AI_RESPONSE_STREAMING, GEN_AI_TOOL_NAME, SENTRY_OP, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; +import { GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -146,9 +147,23 @@ describe('Anthropic integration (1.x)', () => { 'read', 'weather', ]); - for (const toolSpan of toolSpans) { - expect(toolSpan.parent_span_id, toolSpan.name).toBe(main.span_id); + const agentSpans = receivedSpans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT); + expect(agentSpans).toHaveLength(2); + for (const sessionId of ['sesn_weather', 'sesn_direct']) { + const agent = agentSpans.find(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === sessionId)!; + expect(agent.parent_span_id, sessionId).toBe(main.span_id); + const sessionTools = toolSpans.filter(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value === sessionId); + expect(sessionTools.map(span => span.parent_span_id)).toEqual(sessionTools.map(() => agent.span_id)); } + expect( + toolSpans + .map(span => [span.attributes[GEN_AI_TOOL_NAME]?.value, span.attributes[GEN_AI_CONVERSATION_ID]?.value]) + .sort(), + ).toEqual([ + ['explode', 'sesn_weather'], + ['read', 'sesn_direct'], + ['weather', 'sesn_weather'], + ]); const weather = toolSpans.find(span => span.attributes[GEN_AI_TOOL_NAME]?.value === 'weather')!; expect(weather.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'sevt_weather' }); expect(receivedEvent?.exception?.values?.[0]?.value).toBe('Tool exploded'); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-stream-midway-error.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-stream-midway-error.mjs new file mode 100644 index 000000000000..321928dc78bf --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-stream-midway-error.mjs @@ -0,0 +1,103 @@ +import { GoogleGenAI } from '@google/genai'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockGoogleGenAIServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1beta/models/:model\\:streamGenerateContent', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Transfer-Encoding', 'chunked'); + + const chunks = [ + { + candidates: [{ content: { parts: [{ text: 'This stream will ' }], role: 'model' }, index: 0 }], + responseId: 'mock-midway-response-id', + modelVersion: 'gemini-1.5-pro', + usageMetadata: { promptTokenCount: 8, candidatesTokenCount: 4, totalTokenCount: 12 }, + }, + { candidates: [{ content: { parts: [{ text: 'be cut off' }], role: 'model' }, index: 0 }] }, + ]; + chunks.forEach((chunk, index) => { + setTimeout(() => res.write(`data: ${JSON.stringify(chunk)}\n\n`), index * 10); + }); + + // The midway case drops the connection after the chunks are parsed, without a final chunk. + // The other cases need the body to stay open long enough for their abort to hit a pending read. + setTimeout( + () => (req.params.model === 'midway-error' ? res.destroy() : res.end()), + req.params.model === 'midway-error' ? 70 : 1000, + ); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockGoogleGenAIServer(); + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const client = new GoogleGenAI({ + apiKey: 'mock-api-key', + httpOptions: { baseUrl: `http://localhost:${server.address().port}` }, + }); + + try { + const stream = await client.models.generateContentStream({ + model: 'midway-error', + contents: [{ role: 'user', parts: [{ text: 'This will fail midway' }] }], + }); + for await (const _ of stream) { + void _; + } + } catch { + // Error expected + } + + const stream = await client.models.generateContentStream({ + model: 'early-break', + contents: [{ role: 'user', parts: [{ text: 'The caller stops reading after the first chunk' }] }], + }); + for await (const _ of stream) { + break; + } + + const controller = new AbortController(); + try { + const abortedStream = await client.models.generateContentStream({ + model: 'caller-abort', + contents: [{ role: 'user', parts: [{ text: 'The caller aborts after the first chunk' }] }], + config: { abortSignal: controller.signal }, + }); + for await (const _ of abortedStream) { + controller.abort(); + } + } catch { + // Error expected + } + + try { + const timedOutStream = await client.models.generateContentStream({ + model: 'timeout', + contents: [{ role: 'user', parts: [{ text: 'The SDK timeout fires mid-stream' }] }], + config: { httpOptions: { timeout: 100 } }, + }); + for await (const _ of timedOutStream) { + void _; + } + } catch { + // Error expected + } + }); + + await Sentry.flush(2000); + + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 710d167bea2e..c9a1944151d0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -585,4 +585,39 @@ describe('Google GenAI integration', () => { .completed(); }); }); + + createEsmAndCjsTests( + __dirname, + 'scenario-stream-midway-error.mjs', + 'instrument-with-pii.mjs', + (createRunner, test) => { + test('marks the span as errored when the stream fails midway, but not when the caller breaks early', async () => { + await createRunner() + .unordered() + .expect({ + span: container => { + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === EXPECTED_ORIGIN); + expect(spans).toHaveLength(4); + + const midwaySpan = spans.find(span => span.name === 'generate_content midway-error'); + expect(midwaySpan!.status).toBe('error'); + expect(midwaySpan!.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + expect(midwaySpan!.attributes[GEN_AI_RESPONSE_ID]?.value).toBe('mock-midway-response-id'); + expect(midwaySpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); + expect(midwaySpan!.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('This stream will be cut off'); + + const earlyBreakSpan = spans.find(span => span.name === 'generate_content early-break'); + expect(earlyBreakSpan!.status).toBe('ok'); + expect(earlyBreakSpan!.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('This stream will '); + + // `cancelled` is sent as `ok` under span streaming, so a timeout must not be recorded as one. + expect(spans.find(span => span.name === 'generate_content caller-abort')!.status).toBe('ok'); + expect(spans.find(span => span.name === 'generate_content timeout')!.status).toBe('error'); + }, + }) + .start() + .completed(); + }); + }, + ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-stream-scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-stream-scenario.mjs new file mode 100644 index 000000000000..cca20e95102a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-stream-scenario.mjs @@ -0,0 +1,53 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import { HumanMessage, SystemMessage } from '@langchain/core/messages'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_react_agent_stream_123', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'Paris is the capital of France.' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 20, output_tokens: 10 }, + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => resolve(server)); + }); +} + +async function run() { + const server = await startMockAnthropicServer(); + const baseUrl = `http://localhost:${server.address().port}`; + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const llm = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + apiKey: 'mock-api-key', + clientOptions: { baseURL: baseUrl }, + }); + const agent = createReactAgent({ llm, tools: [], name: 'helpful_assistant' }); + + const stream = await agent.stream({ + messages: [new SystemMessage('You are a helpful assistant.'), new HumanMessage('What is the capital of France?')], + }); + for await (const _chunk of stream) { + // Consume the stream so the agent and its child spans complete before the root span ends. + } + }); + + await Sentry.flush(2000); + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-stream.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-stream.mjs new file mode 100644 index 000000000000..747b7d4e11eb --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-stream.mjs @@ -0,0 +1,90 @@ +import { RunnableLambda } from '@langchain/core/runnables'; +import { FakeListChatModel } from '@langchain/core/utils/testing'; +import { END, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; +import * as Sentry from '@sentry/node'; + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +const user = content => ({ messages: [{ role: 'user', content }] }); + +function weatherGraph(name, model) { + return new StateGraph(MessagesAnnotation) + .addNode('agent', async state => ({ messages: [await model.invoke(state.messages)] })) + .addEdge(START, 'agent') + .addEdge('agent', END) + .compile({ name }); +} + +async function run() { + const model = new FakeListChatModel({ responses: ['Sunny'] }); + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const graph = weatherGraph('weather_graph', model); + + for await (const _chunk of await graph.stream(user('Read the whole stream'))) { + // drain + } + + await graph.stream(user('Never read the stream')); + + for await (const _chunk of await graph.stream(user('Break out of the stream'))) { + break; + } + + const canceled = await graph.stream(user('Cancel the stream')); + await canceled.cancel(); + + const failing = new StateGraph(MessagesAnnotation) + .addNode('agent', async () => { + throw new Error('Weather service down'); + }) + .addEdge(START, 'agent') + .addEdge('agent', END) + .compile({ name: 'failing_graph' }); + try { + for await (const _chunk of await failing.stream(user('Fail the stream'))) { + // drain + } + } catch { + // expected + } + + const outer = new StateGraph(MessagesAnnotation) + .addNode('inner', weatherGraph('inner_graph', model)) + .addEdge(START, 'inner') + .addEdge('inner', END) + .compile({ name: 'outer_graph' }); + for await (const _chunk of await outer.stream(user('Stream a subgraph'))) { + // drain + } + + for await (const _event of graph.streamEvents(user('Stream events'), { version: 'v2' })) { + // drain + } + + await graph.invoke(user('Invoke the graph')); + + // stream() called without config inside a parent run must keep reporting to the parent's callbacks. + let parentSawGraphRun = false; + const parentCallback = { + handleChainStart: (_chain, _inputs, _runId, _parentRunId, _tags, metadata) => { + if (metadata?.lc_agent_name === 'weather_graph') { + parentSawGraphRun = true; + } + }, + }; + await RunnableLambda.from(async input => { + for await (const _chunk of await graph.stream(input)) { + // drain + } + }).invoke(user('Stream inside a parent run'), { callbacks: [parentCallback] }); + Sentry.startSpan( + { name: 'parent callbacks', attributes: { 'test.parent_saw_graph_run': parentSawGraphRun } }, + () => {}, + ); + + // Lets the unread stream's run finish before the root span ends. + await sleep(200); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 53dc3f6846af..62d9e6cef300 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -6,7 +6,9 @@ import { GEN_AI_OPERATION_NAME, GEN_AI_OUTPUT_MESSAGES, GEN_AI_PIPELINE_NAME, + GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_SYSTEM_INSTRUCTIONS, @@ -18,6 +20,7 @@ import { SENTRY_OP, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { GEN_AI_CHAT, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { getStringAttributeValue } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -112,6 +115,7 @@ describe('LangGraph integration', () => { expect(toolAgentInvokeSpan!.attributes[GEN_AI_TOOL_DEFINITIONS].value).toContain('get_weather'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_INPUT_MESSAGES].value).toContain('What is the weather?'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gpt-4-0613'); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toBe('["stop"]'); expect(toolAgentInvokeSpan!.attributes[GEN_AI_RESPONSE_TEXT].value).toContain( 'Response without calling tools', ); @@ -365,4 +369,88 @@ describe('LangGraph integration', () => { .completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-stream.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('creates one agent span per stream() run, however the stream is consumed', { timeout: 30000 }, async () => { + await createRunner() + .expect({ + span: container => { + const spans = container.items; + const agentSpans = spans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT); + const agentSpanFor = (prompt: string) => + agentSpans.find(span => + getStringAttributeValue(span.attributes[GEN_AI_INPUT_MESSAGES]?.value)?.includes(prompt), + ); + const chatSpanUnder = (parent: { span_id: string } | undefined) => + spans.find( + span => span.attributes[SENTRY_OP]?.value === GEN_AI_CHAT && span.parent_span_id === parent?.span_id, + ); + + expect(agentSpans).toHaveLength(10); + expect(agentSpanFor('Stream inside a parent run')?.status).toBe('ok'); + const parentCallbacks = spans.find(span => span.name === 'parent callbacks'); + expect(parentCallbacks?.attributes['test.parent_saw_graph_run']?.value).toBe(true); + + const read = agentSpanFor('Read the whole stream'); + expect(read?.status).toBe('ok'); + expect(read?.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + expect(read?.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('[{"role":"assistant","content":"Sunny"}]'); + expect(chatSpanUnder(read)).toBeDefined(); + + const unread = agentSpanFor('Never read the stream'); + expect(unread?.status).toBe('ok'); + expect(unread?.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('[{"role":"assistant","content":"Sunny"}]'); + expect(chatSpanUnder(unread)).toBeDefined(); + + expect(agentSpanFor('Break out of the stream')?.status).toBe('ok'); + expect(agentSpanFor('Cancel the stream')?.status).toBe('ok'); + + const failed = agentSpans.find(span => span.name === 'invoke_agent failing_graph'); + expect(failed?.status).toBe('error'); + + const outer = agentSpans.find(span => span.name === 'invoke_agent outer_graph'); + const inner = agentSpans.find(span => span.name === 'invoke_agent inner_graph'); + expect(outer?.status).toBe('ok'); + expect(inner?.parent_span_id).toBe(outer?.span_id); + + expect(agentSpanFor('Stream events')?.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + + const invoked = agentSpanFor('Invoke the graph'); + expect(invoked?.status).toBe('ok'); + expect(invoked?.attributes[GEN_AI_RESPONSE_STREAMING]).toBeUndefined(); + }, + }) + .unordered() + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'agent-stream-scenario.mjs', 'instrument-agent.mjs', (createRunner, test) => { + test( + 'parents the chat span of a streamed createReactAgent run under its agent span', + { timeout: 30000 }, + async () => { + await createRunner() + .expect({ + span: container => { + const agentSpans = container.items.filter( + span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT, + ); + const chatSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_CHAT); + expect(agentSpans).toHaveLength(1); + expect(chatSpans).toHaveLength(1); + expect(agentSpans[0]!.name).toBe('invoke_agent helpful_assistant'); + expect(agentSpans[0]!.status).toBe('ok'); + expect(agentSpans[0]!.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + expect(chatSpans[0]!.parent_span_id).toBe(agentSpans[0]!.span_id); + expect(chatSpans[0]!.attributes[GEN_AI_AGENT_NAME]?.value).toBe('helpful_assistant'); + }, + }) + .unordered() + .start() + .completed(); + }, + ); + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/scenario-stream.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/scenario-stream.mjs new file mode 100644 index 000000000000..ae9d9a6e0bef --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/scenario-stream.mjs @@ -0,0 +1,102 @@ +import { RunnableLambda } from '@langchain/core/runnables'; +import { FakeListChatModel } from '@langchain/core/utils/testing'; +import { Command, END, interrupt, MemorySaver, MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; +import * as Sentry from '@sentry/node'; + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +const user = content => ({ messages: [{ role: 'user', content }] }); +const drain = async stream => { + for await (const _chunk of stream) { + // drain + } +}; + +function weatherBuilder(model) { + return new StateGraph(MessagesAnnotation) + .addNode('agent', async state => ({ messages: [await model.invoke(state.messages)] })) + .addEdge(START, 'agent') + .addEdge('agent', END); +} + +async function run() { + const model = new FakeListChatModel({ responses: ['Sunny'] }); + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const graph = weatherBuilder(model).compile({ name: 'weather_graph' }); + + await drain(await graph.stream(user('Read the whole stream'))); + await graph.stream(user('Never read the stream')); + for await (const _chunk of await graph.stream(user('Break out of the stream'))) { + break; + } + const canceled = await graph.stream(user('Cancel the stream')); + await canceled.cancel(); + + const slow = new StateGraph(MessagesAnnotation) + .addNode('agent', async () => { + await sleep(200); + return { messages: [] }; + }) + .addEdge(START, 'agent') + .addEdge('agent', END) + .compile({ name: 'slow_graph' }); + const controller = new AbortController(); + setTimeout(() => controller.abort(), 50); + try { + await drain(await slow.stream(user('Abort the stream'), { signal: controller.signal })); + } catch { + // expected + } + + const failing = new StateGraph(MessagesAnnotation) + .addNode('agent', async () => { + throw new Error('Weather service down'); + }) + .addEdge(START, 'agent') + .addEdge('agent', END) + .compile({ name: 'failing_graph' }); + try { + await drain(await failing.stream(user('Fail the stream'))); + } catch { + // expected + } + + const approval = new StateGraph(MessagesAnnotation) + .addNode('ask', async () => ({ messages: [{ role: 'user', content: String(interrupt('Approve?')) }] })) + .addEdge(START, 'ask') + .addEdge('ask', END) + .compile({ name: 'approval_graph', checkpointer: new MemorySaver() }); + const thread = { configurable: { thread_id: 'approval' } }; + await drain(await approval.stream(user('Interrupt the stream'), thread)); + await drain(await approval.stream(new Command({ resume: 'yes' }), thread)); + + await drain(await graph.streamEvents(user('Stream events v3'), { version: 'v3' })); + + const recursive = new StateGraph(MessagesAnnotation) + .addNode('agent', async state => { + if (state.messages.length === 1 && state.messages[0].content === 'Invoke with a nested stream') { + await drain(await recursive.stream(user('Nested stream of the same graph'))); + } + return { messages: [{ role: 'assistant', content: 'done' }] }; + }) + .addEdge(START, 'agent') + .addEdge('agent', END) + .compile({ name: 'recursive_graph' }); + await recursive.invoke(user('Invoke with a nested stream')); + + // Passing the parent's config hands stream() a CallbackManager rather than an array. + await RunnableLambda.from(async (input, config) => drain(await graph.stream(input, config))).invoke( + user('Stream inside a runnable'), + ); + + const manual = Sentry.instrumentStateGraph(weatherBuilder(model), { recordInputs: false }).compile({ + name: 'manual_graph', + }); + await drain(await manual.stream(user('Keep this prompt private'))); + + // Lets the unread stream's run finish before the root span ends. + await sleep(200); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/test.ts index b09261efc29e..1d19f07e0bd1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/v1/test.ts @@ -3,10 +3,12 @@ import { GEN_AI_AGENT_NAME, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_RESPONSE_TEXT, SENTRY_OP, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { GEN_AI_DECIDE } from '@sentry/conventions/op'; +import { GEN_AI_CHAT, GEN_AI_DECIDE, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; describe('LangGraph integration (v1)', () => { @@ -85,4 +87,65 @@ describe('LangGraph integration (v1)', () => { }, }, ); + + createEsmAndCjsTests( + __dirname, + 'scenario-stream.mjs', + 'instrument-with-pii.mjs', + (createRunner, test) => { + test('creates one agent span per stream() run, ended by the graph run', async () => { + const runner = createRunner(); + const spansPromise = runner.collectStreamedSpansUntilSegment('main'); + + await runner.start().completed(); + + const spans = await spansPromise; + const agentSpans = spans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT); + const agentSpanFor = (prompt: string) => + agentSpans.find(span => String(span.attributes[GEN_AI_INPUT_MESSAGES]?.value).includes(prompt)); + const byName = (name: string) => agentSpans.filter(span => span.name === `invoke_agent ${name}`); + + expect(agentSpans).toHaveLength(13); + + for (const prompt of ['Read the whole stream', 'Never read the stream', 'Stream inside a runnable']) { + const span = agentSpanFor(prompt)!; + expect(span.status).toBe('ok'); + expect(span.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + expect(span.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('[{"role":"assistant","content":"Sunny"}]'); + expect( + spans.filter( + child => child.attributes[SENTRY_OP]?.value === GEN_AI_CHAT && child.parent_span_id === span.span_id, + ), + ).toHaveLength(1); + } + + expect(agentSpanFor('Break out of the stream')?.status).toBe('ok'); + expect(agentSpanFor('Cancel the stream')?.status).toBe('ok'); + expect(byName('slow_graph')[0]?.status).toBe('ok'); + expect(byName('failing_graph')[0]?.status).toBe('error'); + + const approvals = byName('approval_graph'); + expect(approvals).toHaveLength(2); + expect(approvals.map(span => span.status)).toEqual(['ok', 'ok']); + + expect(agentSpanFor('Stream events v3')?.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + + const invoked = agentSpanFor('Invoke with a nested stream')!; + const nested = agentSpanFor('Nested stream of the same graph')!; + expect(invoked.attributes[GEN_AI_RESPONSE_STREAMING]).toBeUndefined(); + expect(nested.parent_span_id).toBe(invoked.span_id); + expect(byName('recursive_graph')).toHaveLength(2); + + const manual = byName('manual_graph'); + expect(manual).toHaveLength(1); + expect(manual[0]!.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); + }); + }, + { + additionalDependencies: { + '@langchain/core': '^1.0.0', + '@langchain/langgraph': '^1.0.0', + }, + }, + ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-forward.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-forward.mjs new file mode 100644 index 000000000000..495d52095905 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-forward.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + // The test's fake ingest, so the tunnel forwards the browser's envelopes to it. + dsn: `http://public@localhost:${process.env.INGEST_PORT}/1337`, + release: '1.0', + integrations: [Sentry.serveTunnelIntegration({ intercept: false })], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-intercept-false.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-intercept-false.mjs new file mode 100644 index 000000000000..119324dd7e88 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-intercept-false.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [Sentry.serveTunnelIntegration({ intercept: false })], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-late-emit.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-late-emit.mjs new file mode 100644 index 000000000000..ec92bf06706a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-late-emit.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + // The test's fake ingest, so the tunnel forwards the browser's envelopes to it. + dsn: `http://public@localhost:${process.env.INGEST_PORT}/1337`, + release: '1.0', + integrations: [Sentry.serveTunnelIntegration()], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-no-http.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-no-http.mjs new file mode 100644 index 000000000000..a7e6c2ee4094 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-no-http.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + // The test's fake ingest, so the tunnel forwards the browser's envelopes to it. + dsn: `http://public@localhost:${process.env.INGEST_PORT}/1337`, + release: '1.0', + // Without the HTTP integration, whose `emit` wrapper would answer tunnel requests otherwise. + integrations: integrations => [ + ...integrations.filter(integration => integration.name !== 'Http'), + Sentry.serveTunnelIntegration(), + ], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument.mjs index d06a0abfa862..c5c823df1e08 100644 --- a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument.mjs @@ -5,7 +5,6 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, - // A meta-framework answers the tunnel requests, Node only drops their server spans. integrations: [Sentry.serveTunnelIntegration()], transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-forward.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-forward.mjs new file mode 100644 index 000000000000..8e719e0e2070 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-forward.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node'; +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import http from 'http'; + +const server = http.createServer((request, response) => { + if (!Sentry.handleTunnelRequestIfMatched(request, response)) { + response.end('app'); + } +}); + +server.listen(0, () => { + sendPortToRunner(server.address().port); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-intercept-false.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-intercept-false.mjs new file mode 100644 index 000000000000..709b71b779a8 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-intercept-false.mjs @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node'; +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import http from 'http'; + +const server = http.createServer((request, response) => { + // Middleware that tags the request's user, run here on the tunnel request the app answers itself. + if (request.method === 'POST') { + Sentry.setUser({ id: 'tunnel-user' }); + response.end('app'); + return; + } + + Sentry.captureException(new Error('after tunnel')); + response.end('app'); +}); + +server.listen(0, () => { + sendPortToRunner(server.address().port); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-late-emit.mjs b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-late-emit.mjs new file mode 100644 index 000000000000..6c81781da9c4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-late-emit.mjs @@ -0,0 +1,20 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import http from 'http'; + +// Sentry wraps `emit` on the server instance, so the prototype's is the one from before it. +const emitBeforeSentry = http.Server.prototype.emit; + +const server = http.createServer((request, response) => { + if (request.url === '/replace-emit') { + // Like an app that swaps `emit` for a proxy of the original once the server is up. + server.emit = new Proxy(emitBeforeSentry, {}); + response.end('replaced'); + return; + } + + response.end('app'); +}); + +server.listen(0, () => { + sendPortToRunner(server.address().port); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/test.ts b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/test.ts index 1504ba345677..6e0092279a2f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/test.ts @@ -1,15 +1,100 @@ import { URL_PATH } from '@sentry/conventions/attributes'; import { getTunnelPath } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; +import * as http from 'http'; +import type { AddressInfo } from 'net'; +import * as net from 'net'; +import { afterAll, describe, expect, vi } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; +interface FakeIngest { + port: number; + /** Uploads the tunnel started to Sentry. */ + uploads: number; + /** Envelopes Sentry received in full and answered. */ + delivered: string[]; + /** Uploads that broke off before the whole envelope arrived. */ + brokenUploads: number; + close(): Promise; +} + +/** + * A Sentry ingest that answers late, so the browser can leave while the tunnel waits on it. + */ +async function startFakeIngest(): Promise { + const delivered: string[] = []; + let uploads = 0; + let brokenUploads = 0; + const server = http.createServer((request, response) => { + uploads++; + const chunks: Buffer[] = []; + let ended = false; + request.on('data', chunk => chunks.push(chunk)); + request.on('close', () => { + if (!ended) { + brokenUploads++; + } + }); + request.on('end', () => { + ended = true; + setTimeout(() => { + // The tunnel gave up on the forward. + if (request.socket.destroyed) { + return; + } + response.end('{}'); + delivered.push(Buffer.concat(chunks).toString()); + }, 300); + }); + }); + await new Promise(resolve => server.listen(0, resolve)); + + return { + port: (server.address() as AddressInfo).port, + delivered, + get uploads() { + return uploads; + }, + get brokenUploads() { + return brokenUploads; + }, + close: () => new Promise(resolve => server.close(() => resolve())), + }; +} + +/** + * Posts the first `sentLength` characters of `body` to the tunnel like a browser would, then leaves once the tunnel + * started the upload to Sentry, before it answers. + */ +async function postAndLeave( + port: number, + path: string, + body: string, + ingest: FakeIngest, + sentLength = body.length, +): Promise { + const socket = await new Promise(resolve => { + const connection = net.connect(port, 'localhost', () => resolve(connection)); + }); + socket.write( + `POST ${path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/x-sentry-envelope\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n`, + ); + socket.write(body.slice(0, sentLength)); + // Leaving earlier would only test the tunnel rejecting a body that broke off before it contacted Sentry. + await vi.waitUntil(() => ingest.uploads > 0, { timeout: 5_000 }); + socket.destroy(); +} + +function makeEnvelope(dsn: string, message: string): string { + return [JSON.stringify({ dsn }), JSON.stringify({ type: 'event' }), JSON.stringify({ message })].join('\n'); +} + describe('managed tunnel', () => { afterAll(() => { cleanupChildProcesses(); }); createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => { - test('drops the server span of tunnel requests', async () => { + test('answers tunnel requests before the app, without a server span', async () => { const runner = createRunner() .expect({ span: container => { @@ -20,10 +105,136 @@ describe('managed tunnel', () => { }) .start(); - await runner.makeRequest('post', getTunnelPath('https://public@dsn.ingest.sentry.io/1337')!); + // The tunnel rejects the empty body. The app would have answered with a 200. + await runner.makeRequest('post', getTunnelPath('https://public@dsn.ingest.sentry.io/1337')!, { + expectError: true, + }); await runner.makeRequest('get', '/after-tunnel'); await runner.completed(); }); }); + + createEsmAndCjsTests(__dirname, 'server-forward.mjs', 'instrument-forward.mjs', (createRunner, test) => { + test('delivers an envelope it received in full after the browser leaves', async () => { + const ingest = await startFakeIngest(); + const dsn = `http://public@localhost:${ingest.port}/1337`; + const runner = createRunner() + .withEnv({ INGEST_PORT: String(ingest.port) }) + .start(); + + try { + expect(await runner.makeRequest('get', '/')).toBe('app'); + await postAndLeave(runner.getPort()!, getTunnelPath(dsn)!, makeEnvelope(dsn, 'sent in full'), ingest); + + await vi.waitUntil(() => ingest.delivered.length > 0, { timeout: 5_000 }); + expect(ingest.delivered[0]).toContain('sent in full'); + } finally { + runner.sendSignal('SIGTERM'); + await ingest.close(); + } + }); + + test('breaks off the forward when the browser leaves mid-upload', async () => { + const ingest = await startFakeIngest(); + const dsn = `http://public@localhost:${ingest.port}/1337`; + const runner = createRunner() + .withEnv({ INGEST_PORT: String(ingest.port) }) + .start(); + const envelope = makeEnvelope(dsn, 'a'.repeat(64 * 1024)); + + try { + expect(await runner.makeRequest('get', '/')).toBe('app'); + await postAndLeave(runner.getPort()!, getTunnelPath(dsn)!, envelope, ingest, 1024); + + await vi.waitUntil(() => ingest.brokenUploads > 0, { timeout: 5_000 }); + expect(ingest.delivered).toEqual([]); + } finally { + runner.sendSignal('SIGTERM'); + await ingest.close(); + } + }); + }); + + createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument-no-http.mjs', (createRunner, test) => { + test('answers tunnel requests without the HTTP integration', async () => { + const ingest = await startFakeIngest(); + const dsn = `http://public@localhost:${ingest.port}/1337`; + const runner = createRunner() + .withEnv({ INGEST_PORT: String(ingest.port) }) + .start(); + + try { + const answer = await runner.makeRequest('post', getTunnelPath(dsn)!, { + headers: { 'content-type': 'application/x-sentry-envelope' }, + data: makeEnvelope(dsn, 'without the HTTP integration'), + }); + + expect(answer).not.toBe('app'); + expect(ingest.delivered[0]).toContain('without the HTTP integration'); + expect(await runner.makeRequest('get', '/')).toBe('app'); + } finally { + runner.sendSignal('SIGTERM'); + await ingest.close(); + } + }); + }); + + createEsmAndCjsTests(__dirname, 'server-late-emit.mjs', 'instrument-late-emit.mjs', (createRunner, test) => { + test('still answers tunnel requests after the app replaces `emit`', async () => { + const ingest = await startFakeIngest(); + const dsn = `http://public@localhost:${ingest.port}/1337`; + const runner = createRunner() + .withEnv({ INGEST_PORT: String(ingest.port) }) + .start(); + + try { + expect(await runner.makeRequest('get', '/')).toBe('app'); + expect(await runner.makeRequest('get', '/replace-emit')).toBe('replaced'); + + const answer = await runner.makeRequest('post', getTunnelPath(dsn)!, { + headers: { 'content-type': 'application/x-sentry-envelope' }, + data: makeEnvelope(dsn, 'after emit was replaced'), + }); + + expect(answer).not.toBe('app'); + expect(ingest.delivered[0]).toContain('after emit was replaced'); + } finally { + runner.sendSignal('SIGTERM'); + await ingest.close(); + } + }); + }); + + createEsmAndCjsTests( + __dirname, + 'server-intercept-false.mjs', + 'instrument-intercept-false.mjs', + (createRunner, test) => { + test('records nothing for tunnel requests the app answers, and keeps their scope data isolated', async () => { + const runner = createRunner() + .unordered() + .expect({ + span: container => { + const segmentPaths = container.items + .filter(item => item.is_segment) + .map(item => item.attributes[URL_PATH]); + expect(segmentPaths).toEqual([{ type: 'string', value: '/after-tunnel' }]); + }, + }) + .expect({ + event: event => { + expect(event.exception?.values?.[0]?.value).toBe('after tunnel'); + expect(event.user?.id).toBeUndefined(); + }, + }) + .start(); + + await runner.makeRequest('post', getTunnelPath('https://public@dsn.ingest.sentry.io/1337')!); + await runner.makeRequest('get', '/after-tunnel'); + + await runner.completed(); + }); + }, + ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario-stream-midway-error.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-stream-midway-error.mjs new file mode 100644 index 000000000000..cda82a7fee93 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/scenario-stream-midway-error.mjs @@ -0,0 +1,81 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; +import OpenAI from 'openai'; + +function startMockServer() { + const app = express(); + app.use(express.json()); + + app.post('/openai/chat/completions', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }); + + const chunks = [ + { + id: 'chatcmpl-midway', + object: 'chat.completion.chunk', + model: req.body.model, + choices: [{ index: 0, delta: { role: 'assistant', content: 'This stream will ' } }], + }, + { + id: 'chatcmpl-midway', + object: 'chat.completion.chunk', + model: req.body.model, + choices: [{ index: 0, delta: { content: 'be cut off' } }], + }, + ]; + chunks.forEach((chunk, index) => { + setTimeout(() => res.write(`data: ${JSON.stringify(chunk)}\n\n`), index * 10); + }); + + // The midway case drops the connection after the chunks are parsed, without a final chunk or `[DONE]`. + setTimeout( + () => (req.body.model === 'midway-error' ? res.destroy() : res.end('data: [DONE]\n\n')), + chunks.length * 10 + 50, + ); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockServer(); + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const client = new OpenAI({ + baseURL: `http://localhost:${server.address().port}/openai`, + apiKey: 'mock-api-key', + }); + + try { + const stream = await client.chat.completions.create({ + model: 'midway-error', + messages: [{ role: 'user', content: 'This will fail midway' }], + stream: true, + }); + for await (const _ of stream) { + void _; + } + } catch { + // Error expected + } + + const stream = await client.chat.completions.create({ + model: 'early-break', + messages: [{ role: 'user', content: 'The caller stops reading after the first chunk' }], + stream: true, + }); + for await (const _ of stream) { + break; + } + }); + + await Sentry.flush(2000); + + 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 41b57d713565..a6be1844dbab 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -1692,4 +1692,34 @@ describe('OpenAI integration', () => { expect(rawSpans[0]!.attributes[GEN_AI_USAGE_TOTAL_TOKENS]).toBeUndefined(); }); }); + + createEsmAndCjsTests( + __dirname, + 'scenario-stream-midway-error.mjs', + 'instrument-with-pii.mjs', + (createRunner, test) => { + test('marks the span as errored when the stream fails midway, but not when the caller breaks early', async () => { + await createRunner() + .unordered() + .expect({ + span: container => { + const spans = container.items.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.ai.openai'); + expect(spans).toHaveLength(2); + + const midwaySpan = spans.find(span => span.attributes[GEN_AI_REQUEST_MODEL]?.value === 'midway-error'); + expect(midwaySpan!.status).toBe('error'); + expect(midwaySpan!.attributes[GEN_AI_RESPONSE_STREAMING]?.value).toBe(true); + expect(midwaySpan!.attributes[GEN_AI_RESPONSE_ID]?.value).toBe('chatcmpl-midway'); + expect(midwaySpan!.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('This stream will be cut off'); + + const earlyBreakSpan = spans.find(span => span.attributes[GEN_AI_REQUEST_MODEL]?.value === 'early-break'); + expect(earlyBreakSpan!.status).toBe('ok'); + expect(earlyBreakSpan!.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('This stream will '); + }, + }) + .start() + .completed(); + }); + }, + ); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/segment-name-update/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/segment-name-update/scenario.ts new file mode 100644 index 000000000000..8fb0ac0784b4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/segment-name-update/scenario.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.startSpan({ name: 'GET' }, root => { + Sentry.startSpan({ name: 'early child' }, () => {}); + root.updateName('GET /users/:id'); + Sentry.startSpan({ name: 'late child' }, () => {}); +}); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/tracing/segment-name-update/test.ts b/dev-packages/node-integration-tests/suites/tracing/segment-name-update/test.ts new file mode 100644 index 000000000000..1f951f981b29 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/segment-name-update/test.ts @@ -0,0 +1,27 @@ +import { SENTRY_SEGMENT_NAME } from '@sentry/conventions/attributes'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends the final segment span name on child spans that ended before the segment span was renamed', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: container => { + const segmentNames = Object.fromEntries( + container.items.map(span => [span.name, span.attributes?.[SENTRY_SEGMENT_NAME]?.value]), + ); + + expect(segmentNames).toEqual({ + 'early child': 'GET /users/:id', + 'late child': 'GET /users/:id', + 'GET /users/:id': 'GET /users/:id', + }); + }, + }) + .unordered() + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/typesafe/test.ts b/dev-packages/node-integration-tests/suites/tracing/typesafe/test.ts index ab0b4b68bae2..56e7840fe759 100644 --- a/dev-packages/node-integration-tests/suites/tracing/typesafe/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/typesafe/test.ts @@ -26,110 +26,112 @@ describe('TypeSafe integration', () => { ])('%s instrumentation', (_, scenarioFile, instrumentFile, noRecordingInstrumentFile) => { createEsmAndCjsTests(__dirname, scenarioFile, instrumentFile, (createRunner, test) => { test('creates decide spans for systemOne', async () => { - await createRunner() - .unordered() - .expect({ - span: container => { - const decideSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_DECIDE); - expect(decideSpans).toHaveLength(4); - for (const span of decideSpans) { - expect(span.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.typesafe'); - expect(span.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('decide'); - expect(span.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe('typesafe'); - } + const runner = createRunner(); + const spansPromise = runner.collectStreamedSpans( + spans => + spans.some(span => span.is_segment && span.name === 'main') && + spans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_DECIDE).length >= 4, + ); - const defaultModelSpan = decideSpans.find(span => span.name === 'decide jev-latest')!; - expect(defaultModelSpan).toBeDefined(); - expect(defaultModelSpan.status).toBe('ok'); - expect(defaultModelSpan.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('jev-latest'); - expect(defaultModelSpan.attributes[GEN_AI_RESPONSE_MODEL]?.value).toBe('jev-1.13.0'); - expect(defaultModelSpan.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(275); - expect(defaultModelSpan.attributes[GEN_AI_USAGE_OUTPUT_TOKENS]?.value).toBe(20); - expect(defaultModelSpan.attributes[GEN_AI_USAGE_TOTAL_TOKENS]?.value).toBe(295); - expect(JSON.parse(defaultModelSpan.attributes[GEN_AI_INPUT_MESSAGES]?.value as string)).toEqual([ - { - type: 'evaluation', - state: 'I cannot log in, and I also want a refund for last month.', - questions: { - authIssue: { type: 'noul', instructions: 'Is there a login problem?' }, - department: { - type: 'choice', - instructions: 'Which team should handle this?', - criteria: { billing: 'Charges and refunds', technical: 'Bugs and outages' }, - }, - urgency: { - type: 'score', - instructions: 'How urgent is this ticket?', - criteria: ['low', 'medium', 'high'], - }, - }, - }, - ]); - expect(JSON.parse(defaultModelSpan.attributes[GEN_AI_OUTPUT_MESSAGES]?.value as string)).toEqual([ - { - type: 'evaluation', - answers: { - authIssue: { type: 'noul', noul: 0.98 }, - department: { - type: 'choice', - choice: 'billing', - confidence: 0.28, - probabilities: { billing: 0.64, technical: 0.36 }, - }, - urgency: { - type: 'score', - score: 1.58, - confidence: 0.9, - legend: { 0: 'low', 1: 'medium', 2: 'high' }, - probabilities: { 0: 0, 1: 0.42, 2: 0.58 }, - }, - }, - }, - ]); + await runner.start().completed(); - const asResponseSpan = decideSpans.find(span => span.name === 'decide jev-1.13')!; - expect(asResponseSpan).toBeDefined(); - expect(asResponseSpan.status).toBe('ok'); - expect(asResponseSpan.attributes[GEN_AI_RESPONSE_MODEL]?.value).toBe('jev-1.13.0'); + const decideSpans = (await spansPromise).filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_DECIDE); + expect(decideSpans).toHaveLength(4); + for (const span of decideSpans) { + expect(span.attributes[SENTRY_ORIGIN]?.value).toBe('auto.ai.typesafe'); + expect(span.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('decide'); + expect(span.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe('typesafe'); + } - const errorSpan = decideSpans.find(span => span.name === 'decide error-model')!; - expect(errorSpan).toBeDefined(); - expect(errorSpan.status).toBe('error'); - expect(errorSpan.attributes[GEN_AI_RESPONSE_MODEL]).toBeUndefined(); - expect(errorSpan.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); - - const validationErrorSpan = decideSpans.find(span => span.name === 'decide validation-error')!; - expect(validationErrorSpan).toBeDefined(); - expect(validationErrorSpan.status).toBe('error'); - expect(validationErrorSpan.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); + const defaultModelSpan = decideSpans.find(span => span.name === 'decide jev-latest')!; + expect(defaultModelSpan).toBeDefined(); + expect(defaultModelSpan.status).toBe('ok'); + expect(defaultModelSpan.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('jev-latest'); + expect(defaultModelSpan.attributes[GEN_AI_RESPONSE_MODEL]?.value).toBe('jev-1.13.0'); + expect(defaultModelSpan.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(275); + expect(defaultModelSpan.attributes[GEN_AI_USAGE_OUTPUT_TOKENS]?.value).toBe(20); + expect(defaultModelSpan.attributes[GEN_AI_USAGE_TOTAL_TOKENS]?.value).toBe(295); + expect(JSON.parse(defaultModelSpan.attributes[GEN_AI_INPUT_MESSAGES]?.value as string)).toEqual([ + { + type: 'evaluation', + state: 'I cannot log in, and I also want a refund for last month.', + questions: { + authIssue: { type: 'noul', instructions: 'Is there a login problem?' }, + department: { + type: 'choice', + instructions: 'Which team should handle this?', + criteria: { billing: 'Charges and refunds', technical: 'Bugs and outages' }, + }, + urgency: { + type: 'score', + instructions: 'How urgent is this ticket?', + criteria: ['low', 'medium', 'high'], + }, + }, + }, + ]); + expect(JSON.parse(defaultModelSpan.attributes[GEN_AI_OUTPUT_MESSAGES]?.value as string)).toEqual([ + { + type: 'evaluation', + answers: { + authIssue: { type: 'noul', noul: 0.98 }, + department: { + type: 'choice', + choice: 'billing', + confidence: 0.28, + probabilities: { billing: 0.64, technical: 0.36 }, + }, + urgency: { + type: 'score', + score: 1.58, + confidence: 0.9, + legend: { 0: 'low', 1: 'medium', 2: 'high' }, + probabilities: { 0: 0, 1: 0.42, 2: 0.58 }, + }, }, - }) - .start() - .completed(); + }, + ]); + + const asResponseSpan = decideSpans.find(span => span.name === 'decide jev-1.13')!; + expect(asResponseSpan).toBeDefined(); + expect(asResponseSpan.status).toBe('ok'); + expect(asResponseSpan.attributes[GEN_AI_RESPONSE_MODEL]?.value).toBe('jev-1.13.0'); + + const errorSpan = decideSpans.find(span => span.name === 'decide error-model')!; + expect(errorSpan).toBeDefined(); + expect(errorSpan.status).toBe('error'); + expect(errorSpan.attributes[GEN_AI_RESPONSE_MODEL]).toBeUndefined(); + expect(errorSpan.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); + + const validationErrorSpan = decideSpans.find(span => span.name === 'decide validation-error')!; + expect(validationErrorSpan).toBeDefined(); + expect(validationErrorSpan.status).toBe('error'); + expect(validationErrorSpan.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); }); }); createEsmAndCjsTests(__dirname, scenarioFile, noRecordingInstrumentFile, (createRunner, test) => { test('does not record inputs or outputs when recording is off', async () => { - await createRunner() - .unordered() - .expect({ - span: container => { - const decideSpans = container.items.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_DECIDE); - expect(decideSpans).toHaveLength(4); - expect(decideSpans.filter(span => span.status === 'error')).toHaveLength(2); - for (const span of decideSpans) { - expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); - expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); - // State, questions and answers must not come back through another attribute (e.g. an error message). - // Only the attributes are checked (timestamps could match a number), and `probabilities` only - // occurs in answers. - expect(JSON.stringify(span.attributes)).not.toMatch(/cannot log in|Charges and refunds|probabilities/); - } - }, - }) - .start() - .completed(); + const runner = createRunner(); + const spansPromise = runner.collectStreamedSpans( + spans => + spans.some(span => span.is_segment && span.name === 'main') && + spans.filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_DECIDE).length >= 4, + ); + + await runner.start().completed(); + + const decideSpans = (await spansPromise).filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_DECIDE); + expect(decideSpans).toHaveLength(4); + expect(decideSpans.filter(span => span.status === 'error')).toHaveLength(2); + for (const span of decideSpans) { + expect(span.attributes[GEN_AI_INPUT_MESSAGES]).toBeUndefined(); + expect(span.attributes[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined(); + // State, questions and answers must not come back through another attribute (e.g. an error message). + // Only the attributes are checked (timestamps could match a number), and `probabilities` only + // occurs in answers. + expect(JSON.stringify(span.attributes)).not.toMatch(/cannot log in|Charges and refunds|probabilities/); + } }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs new file mode 100644 index 000000000000..beb57a683162 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs @@ -0,0 +1,42 @@ +import * as Sentry from '@sentry/node'; +import { generateText } from 'ai'; +import { MockLanguageModelV3 } from 'ai/test'; + +const model = () => + new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage: { + inputTokens: { total: 10, noCache: 10, cached: 0 }, + outputTokens: { total: 20, noCache: 20, cached: 0 }, + totalTokens: { total: 30, noCache: 30, cached: 0 }, + }, + content: [{ type: 'text', text: 'ok' }], + warnings: [], + }), + }); + +const brief = { role: 'system', content: 'Be brief.' }; +const kind = { role: 'system', content: 'Be kind.' }; + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + // v6 reads `system`, v7 reads `instructions`. + await generateText({ + experimental_telemetry: { isEnabled: true, recordInputs: true }, + system: brief, + instructions: brief, + model: model(), + prompt: 'hi', + }); + await generateText({ + experimental_telemetry: { isEnabled: true, recordInputs: true }, + system: [brief, kind], + instructions: [brief, kind], + model: model(), + prompt: 'hi', + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index 0ac372b9c59d..ff22a55cc9c3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -1043,6 +1043,36 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe }, ); + createEsmTests( + __dirname, + 'scenario-system-message.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('records system instructions from system message objects', async () => { + await createRunner() + .expect({ + span: container => { + const systemInstructions = container.items + .filter(span => span.attributes[SENTRY_OP]?.value === GEN_AI_INVOKE_AGENT) + .map(span => span.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value); + + expect(systemInstructions).toEqual([ + '[{"type":"text","content":"Be brief."}]', + '[{"type":"text","content":"Be brief."},{"type":"text","content":"Be kind."}]', + ]); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + ai: vercelAiVersion, + }, + }, + ); + createEsmTests( __dirname, 'scenario-openai-conversation.mjs', diff --git a/dev-packages/node-integration-tests/suites/tunnel/hardening/instrument.mjs b/dev-packages/node-integration-tests/suites/tunnel/hardening/instrument.mjs new file mode 100644 index 000000000000..3801c329cbb3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tunnel/hardening/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.serveTunnelIntegration({ allowedDsns: process.env.ALLOWED_DSNS.split(',') })], + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tunnel/hardening/server.mjs b/dev-packages/node-integration-tests/suites/tunnel/hardening/server.mjs new file mode 100644 index 000000000000..0535cf529f2c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tunnel/hardening/server.mjs @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +// Consumes the body of every request, which the tunnel must not be affected by. +app.use(express.json()); + +// The app's own handler on the tunnel path, which the runtime answers before it. +app.post(process.env.TUNNEL_PATH, (_req, res) => { + res.status(418).send('app tunnel handler'); +}); + +app.get('/health', (_req, res) => { + res.send('ok'); +}); + +app.get('/done', (_req, res) => { + Sentry.captureMessage('done'); + res.send('done'); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tunnel/hardening/test.ts b/dev-packages/node-integration-tests/suites/tunnel/hardening/test.ts new file mode 100644 index 000000000000..dac7b16d0277 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tunnel/hardening/test.ts @@ -0,0 +1,39 @@ +import { getTunnelPath } from '@sentry/core'; +import type { FakeIngests } from '@sentry-internal/test-utils'; +import { EXPECTED_TUNNEL_OUTCOMES, runTunnelHardeningCases, startFakeIngests } from '@sentry-internal/test-utils'; +import { afterAll, beforeAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +describe('managed tunnel hardening', () => { + let ingests: FakeIngests; + + beforeAll(async () => { + ingests = await startFakeIngests(); + }); + + afterAll(async () => { + cleanupChildProcesses(); + await ingests.close(); + }); + + createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => { + test('keeps the app serving whatever is thrown at the tunnel, and reports nothing of its own', async () => { + const runner = createRunner() + .withEnv({ ALLOWED_DSNS: ingests.allDsns().join(','), TUNNEL_PATH: getTunnelPath(ingests.dsn('ok'))! }) + // The first event is the app's own, so the tunnel captured no error along the way. + .expect({ event: { message: 'done' } }) + .start(); + await runner.makeRequest('get', '/health'); + + const results = await runTunnelHardeningCases({ port: runner.getPort()!, ingests, getTunnelPath }); + + expect(results.filter(result => result.health !== '200')).toEqual([]); + expect(Object.fromEntries(results.map(result => [result.name, result.outcome]))).toMatchObject( + EXPECTED_TUNNEL_OUTCOMES, + ); + + await runner.makeRequest('get', '/done'); + await runner.completed(); + }, 120_000); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner/createRunner.ts b/dev-packages/node-integration-tests/utils/runner/createRunner.ts index b592869e9aca..e555f1d483df 100644 --- a/dev-packages/node-integration-tests/utils/runner/createRunner.ts +++ b/dev-packages/node-integration-tests/utils/runner/createRunner.ts @@ -103,7 +103,7 @@ type StartResult = { getPort(): number | undefined; sendSignal(signal: NodeJS.Signals): void; makeRequest( - method: 'get' | 'post' | 'put' | 'delete' | 'patch', + method: 'get' | 'head' | 'post' | 'put' | 'delete' | 'patch', path: string, options?: { headers?: Record; data?: BodyInit; expectError?: boolean; timeout?: number }, ): Promise; @@ -665,8 +665,18 @@ export function createRunner(...paths: string[]) { if (process.env.DEBUG) log('stderr line', output); + // Bun runs these scenarios without the Bun build plugin, so the SDK warns that channel + // injection is missing. That warning is expected here and is not an error. + const unexpectedOutput = + process.env.RUNTIME === 'bun' + ? output + .split('\n') + .filter(line => !line.includes('[Sentry] No diagnostics-channel injection detected.')) + .join('\n') + : output; + // Ignore deprecation warnings for this purpose - if (ensureNoErrorOutput && !`${output}`.includes('DeprecationWarning:')) { + if (ensureNoErrorOutput && unexpectedOutput.trim() && !unexpectedOutput.includes('DeprecationWarning:')) { complete(new Error(`Expected no error output but got: '${output}'`)); } }); @@ -781,7 +791,7 @@ export function createRunner(...paths: string[]) { child?.kill(signal); }, makeRequest: async function ( - method: 'get' | 'post' | 'put' | 'delete' | 'patch', + method: 'get' | 'head' | 'post' | 'put' | 'delete' | 'patch', path: string, options: { headers?: Record; diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 5105dc3dd7b4..5af39e269001 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -51,17 +51,17 @@ "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "build": "run-s build:transpile build:types", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "build:dev": "pnpm run build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", - "clean": "rimraf -g ./node_modules ./build" + "clean": "node ../../scripts/rm.mjs ./node_modules ./build" }, "peerDependencies": { "@playwright/test": "1.64.1" }, "dependencies": { - "express": "^4.21.2", + "express": "^4.22.3", "ws": "^8.20.1" }, "devDependencies": { diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 3b82c4a81515..ff74981a603d 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -18,6 +18,8 @@ export { } from './event-proxy-server'; export type { SerializedStreamedSpan } from '@sentry/core'; export { isServerSpanAt, makeClientReportEnvelope, watchForTunnelSpan } from './tunnel'; +export { EXPECTED_TUNNEL_OUTCOMES, runTunnelHardeningCases, startFakeIngests } from './tunnel-hardening'; +export type { FakeIngests, TunnelHardeningResult } from './tunnel-hardening'; export { findAbsolutePathImports, diff --git a/dev-packages/test-utils/src/tunnel-hardening.ts b/dev-packages/test-utils/src/tunnel-hardening.ts new file mode 100644 index 000000000000..76b2145d6eeb --- /dev/null +++ b/dev-packages/test-utils/src/tunnel-hardening.ts @@ -0,0 +1,407 @@ +// Throws every request we could think of at a server whose runtime answers managed tunnel requests, and checks the +// app keeps serving after each one. The tunnel must never break the app, whatever the client, the envelope or the +// ingest does. Only Node built-ins, so Bun and Deno tests can import this file directly. +import { Buffer } from 'node:buffer'; +import * as http from 'node:http'; +import * as net from 'node:net'; +import * as zlib from 'node:zlib'; + +type IngestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => void; + +/** + * Fake ingests, one per behavior. Their DSNs are allowed tunnel DSNs, so the tunnel forwards to them. + */ +const INGEST_HANDLERS: Record = { + ok: (req, res) => drain(req, () => res.end('{}')), + hang: () => undefined, + reset: req => req.socket.destroy(), + error500: (req, res) => drain(req, () => res.writeHead(500).end('boom')), + drip: (_req, res) => { + res.writeHead(200); + let n = 0; + const timer = setInterval(() => (n++ < 20 ? res.write('.') : (clearInterval(timer), res.end())), 20); + res.on('close', () => clearInterval(timer)); + }, + huge: (_req, res) => res.end(Buffer.alloc(5e6, 'x')), + earlyReply: (_req, res) => res.writeHead(200).end('{}'), + resetMidResponse: (req, res) => { + res.writeHead(200); + res.write('partial'); + setTimeout(() => req.socket.destroy(), 20); + }, + // Nothing listens on its port. + refused: null, +}; + +export interface FakeIngests { + /** The DSN of the fake ingest with this behavior. */ + dsn(name: keyof typeof INGEST_HANDLERS): string; + /** Every fake ingest DSN, for `allowedDsns`. */ + allDsns(): string[]; + close(): Promise; +} + +/** + * Starts the fake ingests on free ports. + */ +export async function startFakeIngests(): Promise { + const servers: http.Server[] = []; + const sockets = new Set(); + const ports: Record = {}; + + for (const [name, handler] of Object.entries(INGEST_HANDLERS)) { + const server = http.createServer(handler ?? (() => undefined)); + server.on('connection', socket => { + sockets.add(socket); + socket.on('close', () => sockets.delete(socket)); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + ports[name] = (server.address() as net.AddressInfo).port; + if (handler) { + servers.push(server); + } else { + // Keep the port, refuse connections. + await new Promise(resolve => server.close(() => resolve())); + } + } + + const dsn = (name: string): string => `http://${name}@127.0.0.1:${ports[name]}/1`; + + return { + dsn, + allDsns: () => Object.keys(INGEST_HANDLERS).map(dsn), + close: async () => { + sockets.forEach(socket => socket.destroy()); + await Promise.all(servers.map(server => new Promise(resolve => server.close(resolve)))); + }, + }; +} + +export interface TunnelHardeningOptions { + /** The port of the app under test. */ + port: number; + ingests: FakeIngests; + /** `getTunnelPath` from `@sentry/core`, passed in so this file has no dependencies. */ + getTunnelPath: (dsn: string) => string | undefined; + /** The app's path prefix, if it serves the tunnel under one. */ + basePath?: string; + /** Skips cases by name, for runtimes whose HTTP stack rejects them before any JavaScript runs. */ + skip?: string[]; +} + +export interface TunnelHardeningResult { + name: string; + /** The response status(es) the client got, or what happened instead. */ + outcome: string; + /** The status of a request to the app's `GET /health` right after the case. */ + health: string; +} + +/** + * Runs every case against the app and returns what happened. The app must answer `GET /health` with a 200. + */ +export async function runTunnelHardeningCases(options: TunnelHardeningOptions): Promise { + const results: TunnelHardeningResult[] = []; + for (const [name, run] of getCases(options)) { + if (options.skip?.includes(name)) { + continue; + } + + const outcome = await run(); + results.push({ name, outcome, health: await request(options.port, 'GET', '/health') }); + } + + return results; +} + +/** + * The outcomes the tunnel itself decides, as opposed to the runtime's HTTP parser, so every runtime should agree. + */ +export const EXPECTED_TUNNEL_OUTCOMES: Record = { + 'valid envelope': '200', + 'trailing slash': '200', + 'query string': '200', + // The browser SDK always sends `Content-Length`, and the forward streams up to it. + chunked: '411', + 'invalid JSON header': '400', + 'dsn is an object': '400', + '__proto__ header': '400', + // A browser compresses item payloads, never the whole envelope. + 'gzip encoded': '400', + 'dsn not allowed': '403', + 'other allowed dsn on this path': '500', + '2MB body': '200', + 'slow body': '200', + 'pipelined tunnel then app': '200,200', + 'rejected tunnel with a big unread body, then app': '403,200', + 'pipelined app, tunnel, app': '200,200,200', + 'ingest error500': '500', + 'ingest refused': '500', + 'ingest earlyReply': '200', +}; + +type Case = [name: string, run: () => Promise]; + +function getCases({ port, ingests, getTunnelPath, basePath = '' }: TunnelHardeningOptions): Case[] { + const tunnelPath = (name: string): string => `${basePath}${getTunnelPath(ingests.dsn(name))}`; + const envelope = (name: string, body = '{"type":"event"}\n{"message":"hardening"}\n'): string => + `{"dsn":"${ingests.dsn(name)}"}\n${body}`; + const raw = (chunks: Chunk | Chunk[], rawOptions?: RawOptions): Promise => + rawRequest(port, chunks, rawOptions); + const P = tunnelPath('ok'); + const OK = envelope('ok'); + + return [ + // Matching + ['valid envelope', () => raw(post(P, OK))], + ['GET tunnel path', () => raw(get(P))], + ['HEAD tunnel path', () => raw(`HEAD ${P} HTTP/1.1\r\nHost: localhost\r\n\r\n`)], + ['PUT tunnel path', () => raw(post(P, OK).replace('POST', 'PUT'))], + ['OPTIONS tunnel path', () => raw(`OPTIONS ${P} HTTP/1.1\r\nHost: localhost\r\n\r\n`)], + ['OPTIONS *', () => raw('OPTIONS * HTTP/1.1\r\nHost: localhost\r\n\r\n')], + ['trailing slash', () => raw(post(`${P}/`, OK))], + ['query string', () => raw(post(`${P}?a=1&b`, OK))], + ['double slash', () => raw(post(`/${P}`, OK))], + ['uppercase path', () => raw(post(P.toUpperCase(), OK))], + [ + 'percent-encoded path', + () => + raw( + post( + P.replace(/[^/]/g, c => `%${c.charCodeAt(0).toString(16)}`), + OK, + ), + ), + ], + ['dot segments', () => raw(post(`/x/..${P}`, OK))], + ['absolute-form URL', () => raw(post(`http://evil.example${P}`, OK))], + ['bad Host header', () => raw(post(P, OK).replace('Host: localhost', 'Host: [::bad'))], + ['no Host header', () => raw(post(P, OK).replace('Host: localhost\r\n', ''))], + ['HTTP/1.0', () => raw(post(P, OK).replace('HTTP/1.1', 'HTTP/1.0'))], + ['null byte in path', () => raw(post(`${P}%00`, OK))], + ['8KB query', () => raw(post(`${P}?${'a'.repeat(8000)}`, OK))], + // Bodies + ['empty body', () => raw(post(P, ''))], + ['no newline', () => raw(post(P, `{"dsn":"${ingests.dsn('ok')}"}`))], + ['invalid JSON header', () => raw(post(P, '{"dsn":\n{}\n'))], + ['dsn is a number', () => raw(post(P, '{"dsn":42}\n{}\n'))], + ['dsn is an array', () => raw(post(P, '{"dsn":["a"]}\n{}\n'))], + ['dsn is an object', () => raw(post(P, '{"dsn":{"toString":1}}\n{}\n'))], + ['dsn is null', () => raw(post(P, '{"dsn":null}\n{}\n'))], + ['header is an array', () => raw(post(P, '[1,2]\n{}\n'))], + ['header is a string', () => raw(post(P, '"hi"\n{}\n'))], + ['__proto__ header', () => raw(post(P, `{"__proto__":{"dsn":"${ingests.dsn('ok')}"}}\n{}\n`))], + ['dsn not allowed', () => raw(post(P, '{"dsn":"https://k@o1.ingest.sentry.io/1"}\n{}\n'))], + ['dsn garbage string', () => raw(post(P, '{"dsn":"::::"}\n{}\n'))], + ['other allowed dsn on this path', () => raw(post(P, envelope('error500')))], + ['70KB header line', () => raw(post(P, `{"dsn":"${ingests.dsn('ok')}","pad":"${'x'.repeat(70_000)}"}\n{}\n`))], + [ + 'binary garbage', + () => raw(post(P, Buffer.from(Array.from({ length: 5000 }, (_, i) => (i * 37) % 256)).toString('latin1'))), + ], + ['invalid UTF-8', () => raw(post(P, '\xff\xfe\xfd\n\xc3\x28\n'))], + [ + 'gzip encoded', + () => { + const gzipped = zlib.gzipSync(OK); + + return raw([ + `POST ${P} HTTP/1.1\r\nHost: localhost\r\nContent-Length: ${gzipped.length}\r\nContent-Encoding: gzip\r\n\r\n`, + gzipped, + ]); + }, + ], + ['gzip header, plain body', () => raw(post(P, OK, 'Content-Encoding: gzip\r\n'))], + [ + '2MB body', + () => raw(post(P, envelope('ok', `{"type":"attachment"}\n${'a'.repeat(2e6)}\n`)), { timeoutMs: 10_000 }), + ], + ['chunked', () => raw(chunked(P, OK))], + ['chunked + trailers', () => raw(chunked(P, OK, 'Trailer: X-T\r\n', 'X-T: 1\r\n'))], + ['bad chunk size', () => raw(`POST ${P} HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n`)], + [ + 'Content-Length and Transfer-Encoding', + () => + raw( + `POST ${P} HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n`, + ), + ], + [ + 'duplicate Content-Length', + () => raw(`POST ${P} HTTP/1.1\r\nHost: localhost\r\nContent-Length: 3\r\nContent-Length: 4\r\n\r\nabcd`), + ], + ['Expect: 100-continue', () => raw(post(P, OK, 'Expect: 100-continue\r\n'), { statuses: 2 })], + [ + 'Upgrade: websocket', + () => + raw( + get( + P, + 'Connection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n', + ), + ), + ], + // Client misbehavior + [ + 'abort before the envelope header ends', + () => raw(withContentLength(post(P, OK).slice(0, -OK.length + 10), 9999), { closeAfterMs: 100 }), + ], + ['abort mid-body', () => raw(withContentLength(post(P, OK), 99_999), { closeAfterMs: 100 })], + ['Content-Length too large, then wait', () => raw(withContentLength(post(P, OK), 99_999), { timeoutMs: 1500 })], + ['abort while forwarding', () => raw(post(tunnelPath('hang'), envelope('hang')), { closeAfterMs: 300 })], + [ + 'slow body', + () => + raw( + [ + withContentLength(post(P, ''), Buffer.byteLength(OK)), + ...[...OK.match(/.{1,8}/gs)!].flatMap(chunk => [chunk, 20]), + ], + { timeoutMs: 10_000 }, + ), + ], + ['half-close after body', () => raw(post(P, OK), { endAfterWrite: true })], + // Keep-alive: the tunnel must leave the connection usable for the app. + ['pipelined tunnel then app', () => raw(post(P, OK) + get('/health'), { statuses: 2 })], + [ + 'rejected tunnel with a big unread body, then app', + () => + raw(post(P, `{"dsn":"https://k@o1.ingest.sentry.io/1"}\n${'x'.repeat(200_000)}`) + get('/health'), { + statuses: 2, + }), + ], + ['pipelined app, tunnel, app', () => raw(get('/health') + post(P, OK) + get('/health'), { statuses: 3 })], + // Ingest misbehavior + ...(['hang', 'reset', 'error500', 'drip', 'huge', 'earlyReply', 'resetMidResponse', 'refused'] as const).map( + (name): Case => [ + `ingest ${name}`, + () => raw(post(tunnelPath(name), envelope(name)), { timeoutMs: name === 'hang' ? 1500 : 8000 }), + ], + ), + // Load + [ + '100 mixed concurrent requests', + async () => { + const jobs = Array.from({ length: 100 }, (_, i) => + [ + () => request(port, 'POST', P, OK), + () => request(port, 'GET', '/health'), + () => raw(post(tunnelPath('reset'), envelope('reset'))), + () => raw(post(tunnelPath('hang'), envelope('hang')), { closeAfterMs: 200 }), + ][i % 4]!(), + ); + const counts: Record = {}; + for (const outcome of await Promise.all(jobs)) { + counts[outcome] = (counts[outcome] ?? 0) + 1; + } + + return Object.entries(counts) + .map(([outcome, count]) => `${outcome}x${count}`) + .join(' '); + }, + ], + ]; +} + +type Chunk = string | Buffer | number; + +interface RawOptions { + /** Destroys the socket after this many ms. */ + closeAfterMs?: number; + timeoutMs?: number; + /** How many response status lines to wait for. */ + statuses?: number; + /** Half-closes the socket after writing. */ + endAfterWrite?: boolean; +} + +/** + * Writes raw bytes to the app (numbers are pauses in ms) and returns the response status(es), or what happened + * instead: `aborted by client`, `timeout`, `closed`, or a socket error code. + */ +function rawRequest( + port: number, + chunks: Chunk | Chunk[], + { closeAfterMs, timeoutMs = 3000, statuses = 1, endAfterWrite = false }: RawOptions = {}, +): Promise { + return new Promise(resolve => { + const socket = net.connect(port, '127.0.0.1'); + let data = ''; + let settled = false; + const statusesSoFar = (): string[] => [...data.matchAll(/HTTP\/1\.[01] (\d{3})/g)].map(match => match[1]!); + const finish = (result: string): void => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeout); + socket.destroy(); + resolve(result); + }; + const timeout = setTimeout(() => finish(statusesSoFar().join(',') || 'timeout'), timeoutMs); + + socket.on('data', chunk => { + data += chunk.toString('latin1'); + if (statusesSoFar().length >= statuses) { + finish(statusesSoFar().join(',')); + } + }); + socket.on('error', error => finish(`socket ${(error as Error & { code?: string }).code}`)); + socket.on('close', () => finish(statusesSoFar().join(',') || (data ? 'garbled' : 'closed'))); + socket.on('connect', async () => { + for (const chunk of ([] as Chunk[]).concat(chunks)) { + if (typeof chunk === 'number') { + await new Promise(r => setTimeout(r, chunk)); + } else if (!socket.destroyed) { + socket.write(chunk); + } + } + if (endAfterWrite && !socket.destroyed) { + socket.end(); + } + if (closeAfterMs !== undefined) { + setTimeout(() => finish('aborted by client'), closeAfterMs); + } + }); + }); +} + +/** + * Makes a request with `fetch` and returns the status, or the error code. + */ +async function request(port: number, method: string, path: string, body?: string): Promise { + try { + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + body, + signal: AbortSignal.timeout(5000), + }); + await response.arrayBuffer(); + + return String(response.status); + } catch (error) { + return `fetch ${(error as { cause?: { code?: string } }).cause?.code ?? (error as Error).name}`; + } +} + +function post(path: string, body: string, headers = ''): string { + return `POST ${path} HTTP/1.1\r\nHost: localhost\r\nContent-Length: ${Buffer.byteLength(body)}\r\n${headers}\r\n${body}`; +} + +function get(path: string, headers = ''): string { + return `GET ${path} HTTP/1.1\r\nHost: localhost\r\n${headers}\r\n`; +} + +function chunked(path: string, body: string, headers = '', trailers = ''): string { + return `POST ${path} HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\n${headers}\r\n${Buffer.byteLength(body).toString(16)}\r\n${body}\r\n0\r\n${trailers}\r\n`; +} + +function withContentLength(rawRequest: string, length: number): string { + return rawRequest.replace(/Content-Length: \d+/, `Content-Length: ${length}`); +} + +function drain(req: http.IncomingMessage, done: () => void): void { + req.on('data', () => undefined); + req.on('end', done); + req.on('error', () => undefined); +} diff --git a/docs/repeated-init.md b/docs/repeated-init.md index a8f6445f0ffc..f584225a858b 100644 --- a/docs/repeated-init.md +++ b/docs/repeated-init.md @@ -37,7 +37,7 @@ When `init()` replaces a client, nothing closes the old one: | Entry point | Repeated call | | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `initAndBind` (browser and its wrappers, Deno), Node `_init`, Vercel Edge | Warns, then replaces the client. Returns the new client. | -| Next.js server, Remix server, Hono Node | Keeps the first client and returns it. Logs in debug mode only. | +| Next.js server, Remix server, Astro server, Hono Node | Keeps the first client and returns it. Logs in debug mode only. | | Hono Bun and Deno | Keeps the first client and returns it. Warns with its own text. | | Nuxt server | Keeps the first client and returns it. Logs that a `--import` preload is no longer needed. | | Cloudflare (default) | Keeps the first client of the isolate and returns it, unless that client is closed or closing. `cacheClient: false` makes a new client on each call, with no warning. | diff --git a/nx.json b/nx.json index fc6ead4ce750..ee83272b49d4 100644 --- a/nx.json +++ b/nx.json @@ -63,5 +63,8 @@ "autoExit": true }, "parallel": 5, - "analytics": false + "analytics": false, + "affected": { + "legacy": true + } } diff --git a/package.json b/package.json index dde3b52dcf3a..1b4cd4a2b19b 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "circularDepCheck": "nx run-many -t circularDepCheck", "clean": "run-s clean:build clean:caches", "clean:build": "nx run-many -t clean", - "clean:caches": "pnpm exec rimraf eslintcache .nxcache .nx", - "clean:deps": "rimraf packages/*/node_modules dev-packages/*/node_modules && rm -rf node_modules && pnpm install", - "clean:tarballs": "rimraf {packages,dev-packages}/*/*.tgz", + "clean:caches": "node scripts/rm.mjs eslintcache .nxcache .nx", + "clean:deps": "node scripts/rm.mjs packages/*/node_modules dev-packages/*/node_modules node_modules && pnpm install", + "clean:tarballs": "node scripts/rm.mjs packages/*/*.tgz dev-packages/*/*.tgz", "clean:watchman": "watchman watch-del \".\"", "clean:all": "run-s clean:build clean:tarballs clean:caches clean:deps clean:watchman", "format": "oxfmt . --write", @@ -156,11 +156,10 @@ "madge": "8.0.0", "nodemon": "^3.1.10", "npm-run-all2": "^6.2.0", - "nx": "22.7.10", + "nx": "24.0.3", "oxfmt": "^0.74.0", "oxlint": "^1.89.0", "oxlint-tsgolint": "^7.0.2004", - "rimraf": "^6.1.3", "rollup": "^4.60.3", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-license": "^3.7.1", @@ -168,7 +167,7 @@ "tslib": "^2.4.1", "tsx": "^4.23.15", "typescript": "~7.0.2", - "vitest": "^4.1.11", + "vitest": "^4.1.12", "webpack": "^5.112.2", "yalc": "^1.0.0-pre.53" }, diff --git a/packages/angular/package.json b/packages/angular/package.json index d8f5de765310..b42f359ad95b 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -23,7 +23,7 @@ "dependencies": { "@sentry/browser": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "tslib": "^2.4.1" }, "devDependencies": { @@ -49,9 +49,9 @@ "build:watch": "pnpm run build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "ng build --configuration production --watch", - "build:tarball": "npm pack ./build", + "build:tarball": "pnpm --dir build pack --pack-destination ..", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-angular-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-angular-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/astro/package.json b/packages/astro/package.json index b090296ee81a..2e12f87e3a60 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -59,7 +59,7 @@ "dependencies": { "@sentry/browser": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", @@ -68,7 +68,7 @@ "devDependencies": { "@sentry-internal/rollup-utils": "11.11.0", "astro": "^4.16.19", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "scripts": { "build": "run-p build:transpile build:types", @@ -78,9 +78,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-astro-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-astro-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 76bae75d5973..36c6421b72d7 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -198,7 +198,9 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // This ensures Sentry.init() runs in dev mode (where the Vite plugin doesn't fire) // and also serves as the fallback for non-Cloudflare adapters in production. if (serverInitSnippet) { - updateConfig({ vite: { plugins: [makeServerInitVitePlugin(serverInitSnippet)] } }); + updateConfig({ + vite: { plugins: [makeServerInitVitePlugin(serverInitSnippet, { injectIntoEntry: !isCloudflare })] }, + }); injectScript('page-ssr', SERVER_INIT_IMPORT); } @@ -228,7 +230,11 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ updateConfig({ vite: { - plugins: [sentryCloudflareNodeWarningPlugin()], + // A Pages build gets the same `withSentry` wrap as Workers, which answers managed tunnel requests. + plugins: + command !== 'dev' + ? [sentryCloudflareNodeWarningPlugin(), sentryCloudflareVitePlugin()] + : [sentryCloudflareNodeWarningPlugin()], ssr: { // @sentry/node is required in case we have 2 different @sentry/node // packages installed in the same project. diff --git a/packages/astro/src/integration/middleware/index.ts b/packages/astro/src/integration/middleware/index.ts index 07a7bab9e55f..76f977fa0966 100644 --- a/packages/astro/src/integration/middleware/index.ts +++ b/packages/astro/src/integration/middleware/index.ts @@ -2,7 +2,7 @@ import { handleRequest } from '../../server/middleware'; // The one `@sentry/astro` entry that resolves to server code on every runtime, so the integration's virtual module // imports it from here. -export { _INTERNAL_setManagedTunnelDefaults } from '@sentry/core/server'; +export { _INTERNAL_setTunnelDefaults } from '@sentry/core/server'; type MiddlewareNext = () => Promise; type MiddlewareHandler = (ctx: unknown, next: MiddlewareNext) => Promise | Response | Promise | void; diff --git a/packages/astro/src/integration/serverInit.ts b/packages/astro/src/integration/serverInit.ts index 111ad3c06b42..830ba6c20f46 100644 --- a/packages/astro/src/integration/serverInit.ts +++ b/packages/astro/src/integration/serverInit.ts @@ -14,12 +14,18 @@ export const SERVER_INIT_IMPORT = `import "${SERVER_INIT_MODULE}";\n`; /** * Serves the server init as one module that SSR pages and the Sentry middleware both import, so it runs once, on - * whichever request comes first. With only the `page-ssr` script, a request that renders no SSR page, like a tunnel - * request from a prerendered page on a cold server, would find no client. + * whichever request comes first. With `injectIntoEntry`, a build also imports it from the server entry, so the SDK is + * set up before the server takes its first request: a tunnel request from a prerendered page on a cold server renders + * no SSR page and would otherwise reach the app before the runtime answers tunnel requests. */ -export function makeServerInitVitePlugin(serverInitSnippet: string): VitePlugin { +export function makeServerInitVitePlugin(serverInitSnippet: string, { injectIntoEntry = false } = {}): VitePlugin { + let isBuild = false; + return { name: 'sentry-astro-server-init', + configResolved: config => { + isBuild = config.command === 'build'; + }, resolveId: id => (id === SERVER_INIT_MODULE || id === SENTRY_MIDDLEWARE_MODULE ? `\0${id}` : null), load: id => { if (id === `\0${SERVER_INIT_MODULE}`) { @@ -30,5 +36,13 @@ export function makeServerInitVitePlugin(serverInitSnippet: string): VitePlugin ? `${SERVER_INIT_IMPORT}export { onRequest } from "@sentry/astro/middleware";\n` : null; }, + transform(code, id, options) { + // The dev server's module graph has no server entry, and an import added to what it marks as one hangs it. + if (!injectIntoEntry || !isBuild || !options?.ssr || !this.getModuleInfo(id)?.isEntry) { + return null; + } + + return { code: `${SERVER_INIT_IMPORT}${code}`, map: null }; + }, }; } diff --git a/packages/astro/src/integration/tunnel.ts b/packages/astro/src/integration/tunnel.ts index edf6c3c0b8c4..3d1e489c2e5c 100644 --- a/packages/astro/src/integration/tunnel.ts +++ b/packages/astro/src/integration/tunnel.ts @@ -36,7 +36,7 @@ export function makeTunnelOptionsVitePlugin(tunnelOptions: BrowserTunnelOptions) } return options?.ssr - ? `import { _INTERNAL_setManagedTunnelDefaults } from '@sentry/astro/middleware';\n_INTERNAL_setManagedTunnelDefaults(${JSON.stringify({ basePath: tunnelOptions.basePath })});` + ? `import { _INTERNAL_setTunnelDefaults } from '@sentry/astro/middleware';\n_INTERNAL_setTunnelDefaults(${JSON.stringify({ basePath: tunnelOptions.basePath })});` : `globalThis[${JSON.stringify(TUNNEL_OPTIONS_GLOBAL)}] = ${JSON.stringify(tunnelOptions)};`; }, transform: (code, id) => diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts index 98910a1db2fd..a6ed97979daa 100644 --- a/packages/astro/src/server/sdk.ts +++ b/packages/astro/src/server/sdk.ts @@ -1,11 +1,12 @@ import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; -import { applySdkMetadata } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, debug } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; +import { isDeno } from '@sentry/server-utils'; +import { DEBUG_BUILD } from '../debug-build'; declare const EdgeRuntime: string | undefined; -declare const Deno: unknown; declare const Netlify: unknown; /** @@ -19,6 +20,15 @@ export function init(options: NodeOptions): NodeClient | undefined { return undefined; } + // The integration imports the server config from the server entry and from SSR pages, and a `node --import` + // preload of it is supported too. The first init wins. + const existingClient = _INTERNAL_getActiveClient(); + if (existingClient) { + DEBUG_BUILD && debug.log('SDK already initialized'); + + return existingClient; + } + const opts = { ...options, }; @@ -39,5 +49,5 @@ export function init(options: NodeOptions): NodeClient | undefined { function isEdgeRuntime(): boolean { // Netlify also defines the `Netlify` global in its Node functions, so only Deno + Netlify is its edge. - return typeof EdgeRuntime === 'string' || (typeof Netlify !== 'undefined' && typeof Deno !== 'undefined'); + return typeof EdgeRuntime === 'string' || (typeof Netlify !== 'undefined' && isDeno()); } diff --git a/packages/astro/src/tunnel.ts b/packages/astro/src/tunnel.ts index 5c1b129911d2..8658c09365e8 100644 --- a/packages/astro/src/tunnel.ts +++ b/packages/astro/src/tunnel.ts @@ -1,5 +1,5 @@ import type { BrowserTunnelOptions, Options } from '@sentry/core'; -import { _INTERNAL_applyTunnelDefaults as applyTunnelDefaults, GLOBAL_OBJ } from '@sentry/core'; +import { _INTERNAL_applyBrowserTunnelDefaults as applyBrowserTunnelDefaults, GLOBAL_OBJ } from '@sentry/core'; /** Set by the Sentry Astro integration from the `base` and `trailingSlash` config. */ export const TUNNEL_OPTIONS_GLOBAL = '__sentry_astro_tunnel_options'; @@ -10,5 +10,5 @@ export const TUNNEL_OPTIONS_GLOBAL = '__sentry_astro_tunnel_options'; */ export function applyAstroTunnelOptions(options: Pick): void { const astroTunnelOptions = (GLOBAL_OBJ as { [TUNNEL_OPTIONS_GLOBAL]?: BrowserTunnelOptions })[TUNNEL_OPTIONS_GLOBAL]; - options.tunnel = applyTunnelDefaults(options.tunnel, astroTunnelOptions); + options.tunnel = applyBrowserTunnelDefaults(options.tunnel, astroTunnelOptions); } diff --git a/packages/astro/test/integration/cloudflare.test.ts b/packages/astro/test/integration/cloudflare.test.ts index 95c98f59c3a6..32a5b1ff0793 100644 --- a/packages/astro/test/integration/cloudflare.test.ts +++ b/packages/astro/test/integration/cloudflare.test.ts @@ -176,7 +176,7 @@ describe('Cloudflare Pages vs Workers detection', () => { }); expect(baseConfigHookObject.updateConfig).toHaveBeenCalledWith({ - vite: expect.objectContaining({ plugins: ['sentryCloudflareNodeWarningPlugin'] }), + vite: expect.objectContaining({ plugins: ['sentryCloudflareNodeWarningPlugin', 'sentryCloudflareVitePlugin'] }), }); }); @@ -208,7 +208,7 @@ describe('Cloudflare Pages vs Workers detection', () => { }); expect(baseConfigHookObject.updateConfig).toHaveBeenCalledWith({ - vite: expect.objectContaining({ plugins: ['sentryCloudflareNodeWarningPlugin'] }), + vite: expect.objectContaining({ plugins: ['sentryCloudflareNodeWarningPlugin', 'sentryCloudflareVitePlugin'] }), }); }); @@ -270,7 +270,7 @@ directory = "./dist" }); }); - it('does not add Cloudflare Vite plugins for Pages production build', async () => { + it('wraps the Worker entry with `withSentry` for a Pages production build', async () => { getWranglerConfig.mockReturnValue({ filename: 'wrangler.json', content: JSON.stringify({ @@ -290,9 +290,8 @@ directory = "./dist" command: 'build', }); - // Check that sentryCloudflareVitePlugin is NOT in any of the calls expect(baseConfigHookObject.updateConfig).toHaveBeenCalledWith({ - vite: expect.objectContaining({ plugins: ['sentryCloudflareNodeWarningPlugin'] }), + vite: expect.objectContaining({ plugins: ['sentryCloudflareNodeWarningPlugin', 'sentryCloudflareVitePlugin'] }), }); }); diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index d7bb03651d61..e41615d95242 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -66,7 +66,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", @@ -81,7 +81,7 @@ "scripts": { "build": "run-p build:transpile build:types", "build:extension": "rollup -c rollup.lambda-extension.config.mjs && pnpm exec tsx scripts/buildLambdaExtension.ts", - "build:layer": "rimraf build/aws && pnpm exec tsx scripts/buildLambdaLayer.ts", + "build:layer": "node ../../scripts/rm.mjs build/aws && pnpm exec tsx scripts/buildLambdaLayer.ts", "build:dev": "run-p build:transpile build:types", "build:transpile": "run-s build:transpile:npm build:extension", "build:transpile:npm": "rollup -c rollup.npm.config.mjs", @@ -89,9 +89,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build dist-awslambda-layer coverage sentry-serverless-*.tgz", + "clean": "node ../../scripts/rm.mjs build dist-awslambda-layer coverage sentry-serverless-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/aws-serverless/src/integration/awslambda.ts b/packages/aws-serverless/src/integration/awslambda.ts index 94ddb92d00f6..60642c0e37b5 100644 --- a/packages/aws-serverless/src/integration/awslambda.ts +++ b/packages/aws-serverless/src/integration/awslambda.ts @@ -8,10 +8,10 @@ const SHIM_MODULE_ID = '@sentry/aws-serverless/run-lambda-handler'; function resolveShimFile(): string | undefined { try { - // In the CJS build `require` exists; in the ESM build (and when running the TS source - // directly, e.g. in tests) we create one. Rollup converts `import.meta.url` to an - // equivalent for the CJS build, so both branches are always syntactically valid. - const resolve = typeof require === 'function' ? require.resolve : createRequire(import.meta.url).resolve; + // Never `require` itself, which bundlers like webpack can't follow when it's passed around. A CJS module or bundle + // has `__filename`, while a CJS bundle of the ESM build leaves `import.meta.url` empty. + const resolve = createRequire(typeof __filename === 'string' ? __filename : import.meta.url).resolve; + return resolve(SHIM_MODULE_ID); } catch (error) { DEBUG_BUILD && debug.warn(`Could not resolve ${SHIM_MODULE_ID}, not instrumenting the Lambda handler.`, error); diff --git a/packages/aws-serverless/src/requestSpanOptions.ts b/packages/aws-serverless/src/requestSpanOptions.ts index aa4e6a354f2f..f2843f6ac419 100644 --- a/packages/aws-serverless/src/requestSpanOptions.ts +++ b/packages/aws-serverless/src/requestSpanOptions.ts @@ -40,6 +40,8 @@ import { } from '@sentry/core'; import type { Context } from 'aws-lambda'; +const DEFAULT_PORTS: Record = { http: '80', https: '443' }; + interface ApiGatewayLikeEvent { headers?: Record; path?: string; @@ -122,7 +124,8 @@ function extractFullUrl(event: ApiGatewayLikeEvent): string | undefined { return undefined; } let answer = `${proto}://${host}`; - if (port) { + // Clients put non-default ports into the host header, which ALB passes through as is. + if (port && port !== DEFAULT_PORTS[proto] && !/:\d+$/.test(host)) { answer += `:${port}`; } answer += event.path ?? event.rawPath; diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index 539389f4d576..d5b8fdaa7624 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -1,10 +1,11 @@ import type { Scope } from '@sentry/core'; import { debug } from '@sentry/core'; -import { captureException, captureMessage, flush, getCurrentScope, withScope } from '@sentry/node'; +import { captureException, captureMessage, flush, getCurrentScope, withIsolationScope, withScope } from '@sentry/node'; import type { Context, Handler, StreamifyHandler } from 'aws-lambda'; import { performance } from 'perf_hooks'; import { types } from 'util'; import { DEBUG_BUILD } from './debug-build'; +import { handleStreamingTunnelEvent, handleTunnelEvent } from './tunnel'; import { markEventUnhandled } from './utils'; const { isPromise } = types; @@ -164,8 +165,6 @@ export function wrapHandler( return wrapStreamingHandler(handler, options, START_TIME); } - let timeoutWarningTimer: NodeJS.Timeout | undefined; - // AWSLambda is like Express. It makes a distinction about handlers based on its last argument // async (event) => async handler // async (event, context) => async handler @@ -192,11 +191,16 @@ export function wrapHandler( : (handler as AsyncHandler>); return async (event: TEvent, context: Context) => { - context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; + // Lambda runs no HTTP server, so tunnel requests are answered here, before the handler. + const tunnelResult = handleTunnelEvent(event); + if (tunnelResult) { + return tunnelResult as Promise; + } - timeoutWarningTimer = setupTimeoutWarning(context, options); + context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; async function processResult(): Promise { + const timeoutWarningTimer = setupTimeoutWarning(context, options); const scope = getCurrentScope(); let rv: TResult; @@ -226,7 +230,7 @@ export function wrapHandler( return rv; } - return withScope(async () => { + return withIsolationScope(async () => { return processResult(); }); }; @@ -237,18 +241,20 @@ function wrapStreamingHandler( options: WrapperOptions, startTime: number, ): StreamifyHandler { - let timeoutWarningTimer: NodeJS.Timeout | undefined; - const wrappedHandler = async ( event: TEvent, responseStream: Parameters>[1], context: Context, ): Promise => { - context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; + const tunnelAnswer = handleStreamingTunnelEvent(event, responseStream); + if (tunnelAnswer) { + return tunnelAnswer as Promise; + } - timeoutWarningTimer = setupTimeoutWarning(context, options); + context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; async function processStreamingResult(): Promise { + const timeoutWarningTimer = setupTimeoutWarning(context, options); const scope = getCurrentScope(); try { @@ -274,7 +280,7 @@ function wrapStreamingHandler( } } - return withScope(() => processStreamingResult()); + return withIsolationScope(() => processStreamingResult()); }; const handlerWithSymbols = handler as unknown as Record; diff --git a/packages/aws-serverless/src/tunnel.ts b/packages/aws-serverless/src/tunnel.ts new file mode 100644 index 000000000000..927cc927de82 --- /dev/null +++ b/packages/aws-serverless/src/tunnel.ts @@ -0,0 +1,177 @@ +import { debug } from '@sentry/core'; +import { + _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, + _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, + _INTERNAL_pickForwardedHeaders as pickForwardedHeaders, +} from '@sentry/core/server'; +import { DEBUG_BUILD } from './debug-build'; + +/** + * The parts of an API Gateway (REST and HTTP API), function URL or ALB event the tunnel reads. + */ +interface HttpEvent { + version?: string; + httpMethod?: string; + path?: string; + rawPath?: string; + rawQueryString?: string; + requestContext?: { http?: { method?: string }; elb?: unknown; path?: string }; + headers?: Record | null; + multiValueHeaders?: Record | null; + body?: string | null; + isBase64Encoded?: boolean; +} + +/** + * The result shape API Gateway, function URLs and ALB accept from a handler. + */ +interface HttpResult { + statusCode: number; + headers?: Record; + multiValueHeaders?: Record; + body: string; + isBase64Encoded: boolean; +} + +/** + * The streaming response helper the Lambda runtime provides. + */ +declare const awslambda: + | { + HttpResponseStream: { + from( + stream: unknown, + metadata: { statusCode: number; headers: Record }, + ): { + end(chunk?: Uint8Array): void; + }; + }; + } + | undefined; + +/** + * Answers a managed tunnel request that came in as an HTTP event, before the handler runs. Lambda runs no HTTP server, + * so `wrapHandler` answers it from the handler invocation. Returns `undefined` for every other event. + * + * It never throws. Until a request is claimed, a failure hands the event to the handler. Once claimed, it ends in a + * 502 instead. + */ +export function handleTunnelEvent(event: unknown): Promise | undefined { + const request = toTunnelRequest(event); + if (!request) { + return undefined; + } + + const handled = handleServedTunnelRequest(request); + if (!handled) { + return undefined; + } + + const httpEvent = event as HttpEvent; + + return handled + .then(response => toHttpResult(response, !!httpEvent.multiValueHeaders)) + .catch(() => ({ statusCode: 502, body: '', isBase64Encoded: false })); +} + +/** + * Answers a managed tunnel request for a streaming handler, writing the answer to its response stream. Returns + * `undefined` for every other event, or when the runtime has no `awslambda.HttpResponseStream`. + */ +export function handleStreamingTunnelEvent(event: unknown, responseStream: unknown): Promise | undefined { + if (typeof awslambda === 'undefined' || !awslambda?.HttpResponseStream) { + return undefined; + } + + const request = toTunnelRequest(event); + const handled = request && handleServedTunnelRequest(request); + if (!handled) { + return undefined; + } + + return handled + .then(async response => { + const body = new Uint8Array(await response.arrayBuffer()); + const stream = awslambda.HttpResponseStream.from(responseStream, { + statusCode: response.status, + headers: pickForwardedHeaders(response), + }); + stream.end(body); + }) + .catch(error => { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to answer a streaming tunnel request', error); + // Lambda holds an unended stream until the function timeout. + try { + awslambda?.HttpResponseStream.from(responseStream, { statusCode: 502, headers: {} }).end(); + } catch { + // `from` already wrote the metadata, so only ending the stream is left. + (responseStream as { end(): void }).end(); + } + }); +} + +/** + * Builds a `Request` from an HTTP event that targets the tunnel, or returns `undefined`. Matches on the method and + * path first, so no other event's body gets decoded. + */ +function toTunnelRequest(event: unknown): Request | undefined { + try { + if (!event || typeof event !== 'object') { + return undefined; + } + + const httpEvent = event as HttpEvent; + const method = httpEvent.requestContext?.http?.method ?? httpEvent.httpMethod; + // A REST API's `path` drops a named stage that its `requestContext.path` keeps, like an HTTP API's `rawPath` does. + // Which one is the path the browser posted to depends on the domain, so a REST request is claimed if either + // matches. An ALB event has only `path`. + const paths = + typeof httpEvent.rawPath === 'string' + ? [`${httpEvent.rawPath}${httpEvent.rawQueryString ? `?${httpEvent.rawQueryString}` : ''}`] + : [httpEvent.requestContext?.path, httpEvent.path]; + const path = method && paths.find(candidate => candidate && isServedTunnelRequest({ method, url: candidate })); + if (!method || !path) { + return undefined; + } + + const headers = new Headers(); + for (const [name, value] of Object.entries(httpEvent.headers ?? {})) { + if (value !== undefined) { + headers.set(name, value); + } + } + for (const [name, values] of Object.entries(httpEvent.multiValueHeaders ?? {})) { + if (values?.length && !headers.has(name)) { + headers.set(name, values.join(', ')); + } + } + + // API Gateway base64-encodes binary bodies, like a compressed replay envelope. + const body = httpEvent.body + ? httpEvent.isBase64Encoded + ? Uint8Array.from(atob(httpEvent.body), char => char.charCodeAt(0)) + : new TextEncoder().encode(httpEvent.body) + : new Uint8Array(0); + // The tunnel needs the length, which an event doesn't always carry. + headers.set('content-length', String(body.length)); + + return new Request(`https://${headers.get('host') || 'localhost'}${path}`, { method, headers, body }); + } catch (error) { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to read a Lambda event', error); + + return undefined; + } +} + +async function toHttpResult(response: Response, multiValueHeaders: boolean): Promise { + const headers = pickForwardedHeaders(response); + + return { + statusCode: response.status, + ...(multiValueHeaders + ? { multiValueHeaders: Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, [value]])) } + : { headers }), + body: await response.text(), + isBase64Encoded: false, + }; +} diff --git a/packages/aws-serverless/test/isolation.test.ts b/packages/aws-serverless/test/isolation.test.ts new file mode 100644 index 000000000000..5622c0882611 --- /dev/null +++ b/packages/aws-serverless/test/isolation.test.ts @@ -0,0 +1,87 @@ +import type { Event } from '@sentry/core'; +import { addBreadcrumb, captureException, setUser } from '@sentry/node'; +import type { Context, Handler, StreamifyHandler } from 'aws-lambda'; +import { PassThrough } from 'stream'; +import { beforeEach, describe, expect, test } from 'vitest'; +import { init } from '../src/init'; +import { AWS_HANDLER_STREAMING_RESPONSE, AWS_HANDLER_STREAMING_SYMBOL, wrapHandler } from '../src/sdk'; + +const events: Event[] = []; + +init({ + dsn: 'https://public@example.com/1', + beforeSend(event) { + events.push(event); + return null; + }, +}); + +function createContext(awsRequestId: string, remainingTimeInMillis = 3000): Context { + return { + awsRequestId, + functionName: 'functionName', + functionVersion: '$LATEST', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:functionName', + logGroupName: 'logGroupName', + logStreamName: 'logStreamName', + getRemainingTimeInMillis: () => remainingTimeInMillis, + } as Context; +} + +function handleRequest(event: { user?: string }): void { + if (event.user) { + setUser({ id: event.user }); + addBreadcrumb({ message: `handled request for ${event.user}` }); + } else { + captureException(new Error('anonymous request failed')); + } +} + +function expectNoLeakedData(event: Event | undefined, awsRequestId: string): void { + expect(event?.user).toBeUndefined(); + expect(event?.breadcrumbs).toBeUndefined(); + expect(event?.contexts?.['aws.lambda']?.aws_request_id).toBe(awsRequestId); +} + +describe('wrapHandler isolation', () => { + beforeEach(() => { + events.length = 0; + }); + + test('does not leak isolation scope data between invocations', async () => { + const handler = wrapHandler((async (event: { user?: string }) => handleRequest(event)) as Handler); + + await (handler as any)({ user: 'alice' }, createContext('1')); + await (handler as any)({}, createContext('2')); + + expect(events).toHaveLength(1); + expectNoLeakedData(events[0], '2'); + }); + + test('does not leak isolation scope data between streaming invocations', async () => { + const streamingHandler = (async (event: { user?: string }) => handleRequest(event)) as unknown as StreamifyHandler; + (streamingHandler as any)[AWS_HANDLER_STREAMING_SYMBOL] = AWS_HANDLER_STREAMING_RESPONSE; + const handler = wrapHandler(streamingHandler); + + await (handler as any)({ user: 'alice' }, new PassThrough(), createContext('1')); + await (handler as any)({}, new PassThrough(), createContext('2')); + + expect(events).toHaveLength(1); + expectNoLeakedData(events[0], '2'); + }); + + test('captures the timeout warning with the data of its own invocation', async () => { + const handler = wrapHandler((async (event: { user?: string; wait?: number }) => { + setUser({ id: event.user }); + await new Promise(resolve => setTimeout(resolve, event.wait)); + }) as Handler); + + await (handler as any)({ user: 'alice', wait: 0 }, createContext('1')); + await (handler as any)({ user: 'bob', wait: 50 }, createContext('2', 510)); + + expect(events).toHaveLength(1); + expect(events[0]?.message).toBe('Possible function timeout: functionName'); + expect(events[0]?.user).toEqual({ id: 'bob' }); + expect(events[0]?.contexts?.['aws.lambda']?.aws_request_id).toBe('2'); + }); +}); diff --git a/packages/aws-serverless/test/requestSpanOptions.test.ts b/packages/aws-serverless/test/requestSpanOptions.test.ts new file mode 100644 index 000000000000..03f7870e7db7 --- /dev/null +++ b/packages/aws-serverless/test/requestSpanOptions.test.ts @@ -0,0 +1,37 @@ +import { URL_FULL } from '@sentry/conventions/attributes'; +import type { Context } from 'aws-lambda'; +import { describe, expect, test } from 'vitest'; +import { getRequestSpanOptions } from '../src/requestSpanOptions'; + +const context = { + functionName: 'functionName', + awsRequestId: 'awsRequestId', + invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:functionName', +} as Context; + +function getUrlFull(headers: Record): unknown { + return getRequestSpanOptions({ headers, rawPath: '/users' }, context, false).attributes?.[URL_FULL]; +} + +describe('getRequestSpanOptions', () => { + test.each([ + ['https', '443'], + ['http', '80'], + ])('omits the default port for %s', (proto, port) => { + expect(getUrlFull({ host: 'example.com', 'x-forwarded-proto': proto, 'x-forwarded-port': port })).toBe( + `${proto}://example.com/users`, + ); + }); + + test('keeps a non-default port', () => { + expect(getUrlFull({ host: 'example.com', 'x-forwarded-proto': 'https', 'x-forwarded-port': '8443' })).toBe( + 'https://example.com:8443/users', + ); + }); + + test('does not add the port again when the host header has it', () => { + expect(getUrlFull({ host: 'example.com:8080', 'x-forwarded-proto': 'http', 'x-forwarded-port': '8080' })).toBe( + 'http://example.com:8080/users', + ); + }); +}); diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index aa16049e2e2b..cd5d1965af14 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -34,6 +34,9 @@ vi.mock('@sentry/node', async () => { mockWithScope(fn); return fn(mockScope); }, + withIsolationScope: (fn: (scope: unknown) => unknown) => { + return fn(mockScope); + }, captureMessage: (...args: unknown[]) => { mockCaptureMessage(...args); }, @@ -118,7 +121,7 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(mockWithScope).toBeCalledTimes(2); + expect(mockWithScope).toBeCalledTimes(1); expect(mockCaptureMessage).toBeCalled(); expect(mockScope.setTag).toBeCalledWith('timeout', '1s'); }); @@ -134,7 +137,7 @@ describe('AWSLambda', () => { }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(mockWithScope).toBeCalledTimes(1); + expect(mockWithScope).not.toBeCalled(); expect(mockCaptureMessage).not.toBeCalled(); expect(mockScope.setTag).not.toBeCalledWith('timeout', '1s'); }); @@ -524,7 +527,7 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(streamingHandler); await (wrappedHandler as any)(fakeEvent, mockResponseStream, fakeContext); - expect(mockWithScope).toBeCalledTimes(2); + expect(mockWithScope).toBeCalledTimes(1); expect(mockCaptureMessage).toBeCalled(); expect(mockScope.setTag).toBeCalledWith('timeout', '1s'); }); diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 100f346bfe00..3e6a1aca9802 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "web-vitals": "^6.2.3" }, "devDependencies": { @@ -48,8 +48,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-browser-utils-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-browser-utils-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/browser-utils/src/instrumentation/location.ts b/packages/browser-utils/src/instrumentation/location.ts index 7b6dfc5243a0..5e62a777129e 100644 --- a/packages/browser-utils/src/instrumentation/location.ts +++ b/packages/browser-utils/src/instrumentation/location.ts @@ -6,6 +6,8 @@ import { WINDOW } from '../types'; * * @return the absolute URL when handed a relative URL path, by * combining the current `window.location.origin` with param urlOrPath. + * A path starting with `//` is treated as a path on the current origin, + * not as a protocol-relative URL. * * If param urlOrPath is already a full or an invalid URL, * or URL determination fails, the original param urlOrPath is returned unchanged. @@ -13,7 +15,10 @@ import { WINDOW } from '../types'; */ export function getAbsoluteUrl(urlOrPath: string): string { try { - const url = new URL(urlOrPath, WINDOW.location.origin); + const origin = WINDOW.location.origin; + // Callers pass untyped router arguments, which `new URL` used to stringify for us. + const path = String(urlOrPath); + const url = path.startsWith('//') ? new URL(`${origin}${path}`) : new URL(path, origin); return url.toString(); } catch { // fallback, just do nothing diff --git a/packages/browser-utils/src/performance/elementTiming.ts b/packages/browser-utils/src/performance/elementTiming.ts index 1663f5096c00..d7b334fd07b5 100644 --- a/packages/browser-utils/src/performance/elementTiming.ts +++ b/packages/browser-utils/src/performance/elementTiming.ts @@ -8,6 +8,7 @@ import { UI_ELEMENT_URL, UI_ELEMENT_WIDTH, } from '@sentry/conventions/attributes'; +import { UI_ELEMENT_LOAD_TIME, UI_ELEMENT_RENDER_TIME } from '@sentry/conventions/metrics'; import type { IntegrationFn } from '@sentry/core'; import { browserPerformanceTimeOrigin, defineIntegration, filterCollectedUrl, metrics } from '@sentry/core'; import { addPerformanceInstrumentationHandler } from '../instrumentation/performanceObserver'; @@ -80,14 +81,14 @@ const _elementTimingIntegration = (() => { } if (renderTime > 0) { - metrics.distribution(`ui.element.render_time`, renderTime, { + metrics.distribution(UI_ELEMENT_RENDER_TIME, renderTime, { unit: 'millisecond', attributes: metricAttributes, }); } if (loadTime > 0) { - metrics.distribution(`ui.element.load_time`, loadTime, { + metrics.distribution(UI_ELEMENT_LOAD_TIME, loadTime, { unit: 'millisecond', attributes: metricAttributes, }); diff --git a/packages/browser-utils/test/instrumentation/location.test.ts b/packages/browser-utils/test/instrumentation/location.test.ts index c67820d33fff..8d28b55d7a3b 100644 --- a/packages/browser-utils/test/instrumentation/location.test.ts +++ b/packages/browser-utils/test/instrumentation/location.test.ts @@ -24,4 +24,11 @@ describe('getAbsoluteUrl', () => { it('returns fragments', () => { expect(getAbsoluteUrl('/foo#bar')).toBe('https://sentry.io/foo#bar'); }); + + it.each([ + ['//foo/bar', 'https://sentry.io//foo/bar'], + ['//foo/bar?x=1#h', 'https://sentry.io//foo/bar?x=1#h'], + ])('keeps the current origin for a path starting with `//` (%s)', (path, expected) => { + expect(getAbsoluteUrl(path)).toBe(expected); + }); }); diff --git a/packages/browser-utils/test/performance/elementTiming.test.ts b/packages/browser-utils/test/performance/elementTiming.test.ts index 54fc3fba12da..bc4d20bb7343 100644 --- a/packages/browser-utils/test/performance/elementTiming.test.ts +++ b/packages/browser-utils/test/performance/elementTiming.test.ts @@ -1,4 +1,5 @@ import * as sentryCore from '@sentry/core'; +import { UI_ELEMENT_LOAD_TIME, UI_ELEMENT_RENDER_TIME } from '@sentry/conventions/metrics'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { elementTimingIntegration, startTrackingElementTiming } from '../../src/performance/elementTiming'; import * as browserMetricsInstrumentation from '../../src/instrumentation/performanceObserver'; @@ -71,7 +72,7 @@ describe('elementTimingIntegration', () => { }); expect(distributionSpy).toHaveBeenCalledTimes(1); - expect(distributionSpy).toHaveBeenCalledWith('ui.element.render_time', 150, { + expect(distributionSpy).toHaveBeenCalledWith(UI_ELEMENT_RENDER_TIME, 150, { unit: 'millisecond', attributes: { 'sentry.origin': 'auto.ui.browser.element_timing', @@ -116,11 +117,11 @@ describe('elementTimingIntegration', () => { 'ui.element.width': 1920, 'ui.element.height': 1080, }; - expect(distributionSpy).toHaveBeenCalledWith('ui.element.render_time', 200, { + expect(distributionSpy).toHaveBeenCalledWith(UI_ELEMENT_RENDER_TIME, 200, { unit: 'millisecond', attributes: expectedAttributes, }); - expect(distributionSpy).toHaveBeenCalledWith('ui.element.load_time', 150, { + expect(distributionSpy).toHaveBeenCalledWith(UI_ELEMENT_LOAD_TIME, 150, { unit: 'millisecond', attributes: expectedAttributes, }); diff --git a/packages/browser/package.json b/packages/browser/package.json index fee167322c75..52864ae1b7d7 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@sentry/browser-utils": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/feedback": "11.11.0", "@sentry/replay": "11.11.0", "@sentry/replay-canvas": "11.11.0", @@ -60,9 +60,9 @@ "build:dev:watch": "run-p build:transpile:watch", "build:bundle:watch": "rollup -c rollup.bundle.config.mjs --watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage .rpt2_cache sentry-browser-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage .rpt2_cache sentry-browser-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/browser/src/integrations/bfcacheMetrics.ts b/packages/browser/src/integrations/bfcacheMetrics.ts index 78610dc3b2a2..b6c67b3c1df8 100644 --- a/packages/browser/src/integrations/bfcacheMetrics.ts +++ b/packages/browser/src/integrations/bfcacheMetrics.ts @@ -6,6 +6,11 @@ import { BROWSER_BFCACHE_REASON, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { + BROWSER_BFCACHE_NAVIGATION, + BROWSER_BFCACHE_NOT_RESTORED, + BROWSER_BFCACHE_RELOAD_DURATION, +} from '@sentry/conventions/metrics'; import type { IntegrationFn, SpanAttributes } from '@sentry/core'; import { debug, defineIntegration, getCurrentScope, metrics } from '@sentry/core'; import { resolveCurrentRoute } from '@sentry/browser-utils'; @@ -82,7 +87,7 @@ export const bfcacheMetricsIntegration = defineIntegration( // Measures how expensive the fallback reload was when a back/forward navigation missed bfcache. if (typeof navigationEntry.duration === 'number' && navigationEntry.duration > 0) { - metrics.distribution('browser.bfcache.reload.duration', navigationEntry.duration, { + metrics.distribution(BROWSER_BFCACHE_RELOAD_DURATION, navigationEntry.duration, { unit: 'millisecond', attributes: _withOriginAttr({ [SENTRY_SEGMENT_NAME]: routeName, @@ -106,7 +111,7 @@ export const bfcacheMetricsIntegration = defineIntegration( * Captures a bf navigation as a metric and records the outcome and reason count. */ function _captureBFCacheNavigation(outcome: BFCacheOutcome, reasonCount: number, routeName?: string): void { - metrics.count('browser.bfcache.navigation', 1, { + metrics.count(BROWSER_BFCACHE_NAVIGATION, 1, { attributes: _withOriginAttr({ [BROWSER_BFCACHE_OUTCOME]: outcome, // Attribute should be present if reasons are >= 1 @@ -120,7 +125,7 @@ function _captureBFCacheNavigation(outcome: BFCacheOutcome, reasonCount: number, * Maps a collected reason to a metric and captures/sends it. */ function _captureBFCacheReason({ reason, frame }: CollectedReason, routeName?: string) { - metrics.count('browser.bfcache.not_restored', 1, { + metrics.count(BROWSER_BFCACHE_NOT_RESTORED, 1, { attributes: _withOriginAttr({ [BROWSER_BFCACHE_REASON]: reason, [BROWSER_BFCACHE_FRAME]: frame, diff --git a/packages/bun/package.json b/packages/bun/package.json index bf179627e0df..db2ff14d7e2e 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -45,7 +45,7 @@ "dependencies": { "@sentry/bundler-plugins": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/node": "11.11.0", "@sentry/server-utils": "11.11.0" }, @@ -61,9 +61,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-bun-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-bun-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index a8e107ffdfe8..678df401163e 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -174,7 +174,6 @@ export { spanToTraceHeader, spanToBaggageHeader, trpcMiddleware, - serveTunnelIntegration, getTunnelPath, handleTunnelRequest, handleTunnelRequestIfMatched, @@ -242,6 +241,7 @@ export { initWithoutDefaultIntegrations, } from './sdk'; export { bunServerIntegration } from './integrations/bunserver'; +export { serveTunnelIntegration } from './integrations/serveTunnel'; export type { BunServerIntegrationOptions } from './integrations/bunserver'; export { bunHttpServerIntegration } from './integrations/bunHttpServer'; export { fetchIntegration } from './integrations/fetch'; diff --git a/packages/bun/src/integrations/bunHttpServer.ts b/packages/bun/src/integrations/bunHttpServer.ts index bb423dc5773d..24c02026f688 100644 --- a/packages/bun/src/integrations/bunHttpServer.ts +++ b/packages/bun/src/integrations/bunHttpServer.ts @@ -10,6 +10,7 @@ import type { HttpServerResponse, } from '@sentry/core/server'; import { getHttpServerSubscriptions, HTTP_ON_SERVER_REQUEST } from '@sentry/core/server'; +import { isBun } from '@sentry/server-utils'; const INTEGRATION_NAME = 'BunHttpServer' as const; @@ -135,7 +136,7 @@ export const bunHttpServerIntegration = defineIntegration(_bunHttpServerIntegrat */ export function instrumentBunHttpServer(options: BunHttpServerOptions = {}): void { // This only makes sense under Bun; on Node the diagnostics channel already handles this. - if (!process.versions.bun || hasPatched) { + if (!isBun() || hasPatched) { return; } diff --git a/packages/bun/src/integrations/serveTunnel.ts b/packages/bun/src/integrations/serveTunnel.ts new file mode 100644 index 000000000000..abb0e8608a3e --- /dev/null +++ b/packages/bun/src/integrations/serveTunnel.ts @@ -0,0 +1,31 @@ +import { makeServeTunnelIntegration } from '@sentry/server-utils'; +import { instrumentBunServers } from '../tunnelServer'; + +/** + * Lets your server forward the events of a browser SDK set up with `tunnel: true` to Sentry. + * + * With this integration on the server and `tunnel: true` in the browser `init`, the browser sends its events to your + * own server instead of Sentry, where ad-blockers can't drop them, and your server forwards them to Sentry. + * + * `Bun.serve` (including a default-exported server) and `node:http` servers answer the tunnel requests before the + * app sees them. A server started before `init` isn't covered. To serve them from your own code instead, set + * `intercept: false` and call `handleTunnelRequestIfMatched`. + * + * By default only envelopes for the server's own DSN are forwarded. + * + * @example + * ```js + * // Server + * Sentry.init({ + * dsn: '__DSN__', + * integrations: [Sentry.serveTunnelIntegration()], + * }); + * + * // Browser + * Sentry.init({ + * dsn: '__DSN__', + * tunnel: true, + * }); + * ``` + */ +export const serveTunnelIntegration = makeServeTunnelIntegration(instrumentBunServers); diff --git a/packages/bun/src/tunnelServer.ts b/packages/bun/src/tunnelServer.ts new file mode 100644 index 000000000000..3df1bc78740a --- /dev/null +++ b/packages/bun/src/tunnelServer.ts @@ -0,0 +1,202 @@ +import * as http from 'node:http'; +import * as https from 'node:https'; +import { _INTERNAL_normalizeTunnelBasePath, debug } from '@sentry/core'; +import { + _INTERNAL_getServedTunnel as getServedTunnel, + _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, +} from '@sentry/core/server'; +import { _INTERNAL_instrumentTunnelServer as instrumentTunnelServer } from '@sentry/server-utils'; +import type { Server } from 'bun'; + +type BunServeOptions = Parameters[0] & { routes?: Record }; +type BunHandler = (request: Request, server: Server) => Response | Promise; + +let hasPatchedBunServe = false; +let hasPatchedHttpServer = false; + +/** + * Lets `Bun.serve` (including a default-exported server) and `node:http` servers answer managed tunnel requests + * before the app sees them. Nothing is answered unless the tunnel is on, through `serveTunnelIntegration` or a + * framework SDK. A server started before `init` isn't covered. + */ +export function instrumentBunServers(): void { + patchBunServe(); + patchHttpServer(); +} + +/** + * Answers tunnel requests ahead of every `Bun.serve` app, including after `server.reload`. Bun starts a + * default-exported server through the same global, so it's covered too. + */ +function patchBunServe(): void { + if (hasPatchedBunServe) { + return; + } + + try { + const originalServe = Bun.serve; + Bun.serve = function (this: unknown, serveOptions: BunServeOptions) { + const server = originalServe.call(this, withTunnel(serveOptions)) as Server; + try { + const originalReload = server.reload.bind(server); + server.reload = (reloadOptions: BunServeOptions) => originalReload(withTunnel(reloadOptions)); + } catch (error) { + debug.warn('Sentry tunnel: could not wrap server.reload', error); + } + + return server; + }; + hasPatchedBunServe = true; + } catch (error) { + debug.warn('Sentry tunnel: could not patch Bun.serve', error); + } +} + +/** + * Adds a `POST` route per tunnel path and answers tunnel requests ahead of `fetch`, on a copy of the options. The app's + * own options stay as they were, so passing them again, e.g. to `server.reload`, doesn't wrap our wrappers. Returns + * the app's options if anything goes wrong, so the server still starts. + * + * Routes, not only `fetch`: Bun answers a POST under a `'/*'` HTML import or static `Response` without running any + * JavaScript, so only an exact route reaches the tunnel there. A `POST`-only route leaves every other method to the + * app's routes. + */ +function withTunnel(serveOptions: BunServeOptions): BunServeOptions { + try { + if (!serveOptions || typeof serveOptions !== 'object') { + return serveOptions; + } + + // Keeps the prototype and the getters, which class-based options read Bun's settings through. + const patchedOptions = Object.create( + Object.getPrototypeOf(serveOptions), + Object.getOwnPropertyDescriptors(serveOptions), + ) as BunServeOptions; + const userFetch = serveOptions.fetch as BunHandler | undefined; + if (typeof userFetch === 'function') { + setOption(patchedOptions, 'fetch', function (this: Server, request: Request, server: Server) { + return handleServedTunnelRequest(request) ?? userFetch.call(this, request, server); + }); + } + + const tunnelPaths = getTunnelRoutePaths(); + if (tunnelPaths.length) { + const routes: Record = { ...serveOptions.routes }; + for (const path of tunnelPaths) { + routes[path] = makeTunnelRoute(routes[path], userFetch); + } + setOption(patchedOptions, 'routes', routes); + } + + return patchedOptions; + } catch (error) { + debug.warn('Sentry tunnel: could not add the tunnel to Bun.serve', error); + + return serveOptions; + } +} + +/** + * Sets an option on our copy as its own property, which also replaces an option the app's class reads through a + * getter. + */ +function setOption(options: BunServeOptions, name: 'fetch' | 'routes', value: unknown): void { + Object.defineProperty(options, name, { value, writable: true, enumerable: true, configurable: true }); +} + +/** + * The full tunnel paths, with and without a trailing slash, since Bun routes match exactly. + */ +function getTunnelRoutePaths(): string[] { + const servedTunnel = getServedTunnel(); + if (!servedTunnel) { + return []; + } + + const basePath = _INTERNAL_normalizeTunnelBasePath(servedTunnel.basePath); + + return servedTunnel.paths.flatMap(path => [`${basePath}${path}`, `${basePath}${path}/`]); +} + +/** + * Our `POST` handler, merged with whatever the app has on the same path. An app route that is neither a handler nor + * a handler per method (a static `Response` or an HTML import) is left as it is. + */ +function makeTunnelRoute(existing: unknown, appFetch: BunHandler | undefined): unknown { + if (typeof existing === 'function') { + const appRoute = existing as BunHandler; + + return (request: Request, server: Server) => + (request.method === 'POST' && handleServedTunnelRequest(request)) || appRoute(request, server); + } + + if (existing !== undefined && Object.getPrototypeOf(existing) !== Object.prototype) { + return existing; + } + + const methods = (existing ?? {}) as Record; + const appPost = methods.POST; + + return { + ...methods, + POST: (request: Request, server: Server) => + handleServedTunnelRequest(request) ?? toApp(request, server, appPost, appFetch), + }; +} + +/** + * Where a POST on a tunnel path goes when the tunnel doesn't answer it. Bun can't hand a request back to its router, + * so the app's route on the same path, else `fetch`, else a 404. + */ +function toApp( + request: Request, + server: Server, + appPost: unknown, + appFetch: BunHandler | undefined, +): Response | Promise { + if (appPost instanceof Response) { + return appPost.clone(); + } + + if (typeof appPost === 'function') { + return (appPost as BunHandler)(request, server); + } + + if (appFetch) { + return appFetch.call(server, request, server); + } + + return new Response('Not Found', { status: 404 }); +} + +/** + * Bun publishes no `http.server.request.start` channel, so the first request of each `node:http` server hands it to + * the same `emit` wrapper Node uses, then goes through it. + */ +function patchHttpServer(): void { + if (hasPatchedHttpServer) { + return; + } + + hasPatchedHttpServer = true; + const instrumented = new WeakSet(); + // In Bun `https.Server` reuses `http.Server`. + for (const ServerClass of new Set([http.Server, https.Server])) { + try { + // oxlint-disable-next-line typescript/unbound-method -- called with the server as `this` below + const originalEmit = ServerClass.prototype.emit; + ServerClass.prototype.emit = function (this: http.Server, event: string, ...args: unknown[]): boolean { + if (event === 'request' && !instrumented.has(this)) { + instrumented.add(this); + instrumentTunnelServer({ server: this }); + + return this.emit(event, ...args); + } + + return originalEmit.call(this, event, ...args) as boolean; + } as typeof originalEmit; + } catch (error) { + debug.warn('Sentry tunnel: could not patch the node:http server', error); + } + } +} diff --git a/packages/bun/test/fixtures/tunnelHardeningServer.ts b/packages/bun/test/fixtures/tunnelHardeningServer.ts new file mode 100644 index 000000000000..8a46d9645dd3 --- /dev/null +++ b/packages/bun/test/fixtures/tunnelHardeningServer.ts @@ -0,0 +1,68 @@ +// The app `tunnelHardening.test.ts` runs in its own process: a client in the server's process stops getting timers +// while a tunnel route waits on a hung ingest. +import * as SentryCore from '@sentry/core'; +import * as http from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { init, serveTunnelIntegration } from '../../src'; + +const tunnelPath = process.env.TUNNEL_PATH!; + +init({ + dsn: 'https://username@domain/123', + integrations: [serveTunnelIntegration({ allowedDsns: process.env.ALLOWED_DSNS!.split(',') })], + beforeSend: event => { + // The test fails on any of these. + process.stdout.write(`EVENT ${JSON.stringify(event.exception?.values?.[0] ?? event.message)}\n`); + + return null; + }, + transport: () => + SentryCore.createTransport({ recordDroppedEvent: () => undefined }, () => SentryCore.resolvedSyncPromise({})), +}); + +/** + * Answers `/health`, and the tunnel path like an app that handles it itself, which the runtime answers before it. + */ +function appStatus(pathname: string): number { + if (pathname === '/health') { + return 200; + } + + return pathname === tunnelPath ? 418 : 404; +} + +/** + * Prints the port for the test to read. + */ +function announce(port: number | undefined): void { + process.stdout.write(`PORT ${port}\n`); +} + +switch (process.env.SERVER) { + case 'fetch': + announce( + Bun.serve({ + port: 0, + fetch: request => new Response('app', { status: appStatus(new URL(request.url).pathname) }), + }).port, + ); + break; + case 'routes': + announce( + Bun.serve({ + port: 0, + routes: { + '/health': new Response('ok'), + // A static catch-all answers without running any JavaScript, like an SPA fallback. + '/*': new Response('app catch-all', { status: 418 }), + }, + }).port, + ); + break; + default: { + const server = http.createServer((request, response) => { + response.writeHead(appStatus(request.url ?? '/')).end('app'); + }); + server.listen(0, '127.0.0.1', () => announce((server.address() as AddressInfo).port)); + } +} diff --git a/packages/bun/test/managedTunnel.test.ts b/packages/bun/test/managedTunnel.test.ts index 971596340b7b..1d8dcf4c99d8 100644 --- a/packages/bun/test/managedTunnel.test.ts +++ b/packages/bun/test/managedTunnel.test.ts @@ -10,13 +10,12 @@ describe('managed tunnel', () => { SentryCore.getCurrentScope().setClient(undefined); }); - test('Bun.serve drops the server span of tunnel requests', async () => { + test('Bun.serve answers tunnel requests before the app, without a server span', async () => { const transactions: string[] = []; init({ dsn: DSN, tracesSampleRate: 1, traceLifecycle: 'static', - // A meta-framework answers the tunnel requests, Bun only drops their server spans. integrations: [serveTunnelIntegration()], beforeSendTransaction: (event: TransactionEvent) => { transactions.push(event.request?.url ?? ''); @@ -33,7 +32,8 @@ describe('managed tunnel', () => { method: 'POST', body: 'envelope', }); - expect(await tunnelResponse.text()).toBe('app'); + // Answered by the tunnel, which rejects the body as an envelope, not by the app. + expect(await tunnelResponse.text()).toBe('Invalid envelope'); await (await fetch(`${baseUrl}/after-tunnel`)).text(); await SentryCore.flush(); server.stop(); @@ -41,4 +41,64 @@ describe('managed tunnel', () => { expect(transactions).toHaveLength(1); expect(transactions[0]?.endsWith('/after-tunnel')).toBe(true); }); + + test("leaves the app's options as they were, so passing them again to `reload` works", async () => { + initWithTunnel(); + const appFetch = (): Response => new Response('app'); + const appRoute = (): Response => new Response('route'); + const options = { port: 0, fetch: appFetch, routes: { '/route': appRoute } }; + + const server = Bun.serve(options); + server.reload(options); + server.reload(options); + const baseUrl = `http://localhost:${server.port}`; + + try { + expect(options.fetch).toBe(appFetch); + expect(options.routes).toEqual({ '/route': appRoute }); + expect(await tunnelAnswer(baseUrl)).toBe('Invalid envelope'); + expect(await (await fetch(`${baseUrl}/route`)).text()).toBe('route'); + expect(await (await fetch(`${baseUrl}/other`)).text()).toBe('app'); + } finally { + server.stop(); + } + }); + + test('answers tunnel requests for options a class gives through getters', async () => { + initWithTunnel(); + class AppOptions { + public port = 0; + public get fetch(): () => Response { + return () => new Response('app'); + } + } + + const server = Bun.serve(new AppOptions()); + const baseUrl = `http://localhost:${server.port}`; + + try { + expect(await tunnelAnswer(baseUrl)).toBe('Invalid envelope'); + expect(await (await fetch(`${baseUrl}/other`)).text()).toBe('app'); + } finally { + server.stop(); + } + }); }); + +function initWithTunnel(): void { + init({ + dsn: DSN, + integrations: [serveTunnelIntegration()], + transport: () => + SentryCore.createTransport({ recordDroppedEvent: () => undefined }, () => SentryCore.resolvedSyncPromise({})), + }); +} + +/** + * Posts a non-envelope to the tunnel path. The tunnel rejects it, the app would answer `app`. + */ +async function tunnelAnswer(baseUrl: string): Promise { + const response = await fetch(`${baseUrl}${SentryCore.getTunnelPath(DSN)}`, { method: 'POST', body: 'envelope' }); + + return response.text(); +} diff --git a/packages/bun/test/tunnelHardening.test.ts b/packages/bun/test/tunnelHardening.test.ts new file mode 100644 index 000000000000..95ca8ef82fa5 --- /dev/null +++ b/packages/bun/test/tunnelHardening.test.ts @@ -0,0 +1,107 @@ +import * as SentryCore from '@sentry/core'; +import type { Subprocess } from 'bun'; +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; +import * as path from 'node:path'; +import type { FakeIngests } from '../../../dev-packages/test-utils/src/tunnel-hardening'; +import { + EXPECTED_TUNNEL_OUTCOMES, + runTunnelHardeningCases, + startFakeIngests, +} from '../../../dev-packages/test-utils/src/tunnel-hardening'; + +// Bun answers nothing on a connection whose first request awaits anything while a second one is queued behind it, +// Sentry or not. +const PIPELINED_CASES = [ + 'pipelined tunnel then app', + 'rejected tunnel with a big unread body, then app', + 'pipelined app, tunnel, app', +]; + +describe('managed tunnel hardening', () => { + let ingests: FakeIngests; + + beforeAll(async () => { + ingests = await startFakeIngests(); + }); + + afterAll(async () => { + await ingests.close(); + }); + + test.each([ + ['Bun.serve with a fetch handler', 'fetch'], + ['Bun.serve with routes and a catch-all', 'routes'], + ['node:http', 'node-http'], + ])( + '%s', + async (_name, server) => { + const app = Bun.spawn([process.execPath, path.join(__dirname, 'fixtures/tunnelHardeningServer.ts')], { + env: { + ...process.env, + SERVER: server, + ALLOWED_DSNS: ingests.allDsns().join(','), + TUNNEL_PATH: SentryCore.getTunnelPath(ingests.dsn('ok')), + }, + stdout: 'pipe', + stderr: 'inherit', + }); + + try { + const output = readOutput(app); + const port = Number((await output.waitFor(/PORT (\d+)/))[1]); + + const results = await runTunnelHardeningCases({ + port, + ingests, + getTunnelPath: SentryCore.getTunnelPath, + skip: PIPELINED_CASES, + }); + + expect(results.filter(result => result.health !== '200')).toEqual([]); + expect(Object.fromEntries(results.map(result => [result.name, result.outcome]))).toMatchObject( + Object.fromEntries( + Object.entries(EXPECTED_TUNNEL_OUTCOMES).filter(([name]) => !PIPELINED_CASES.includes(name)), + ), + ); + // The tunnel reports nothing of its own to the app's project. + expect(output.text().match(/^EVENT .*/gm)).toBeNull(); + } finally { + app.kill(); + } + }, + 120_000, + ); +}); + +/** + * Collects the app's stdout. + */ +function readOutput(app: Subprocess<'ignore', 'pipe', 'inherit'>): { + text: () => string; + waitFor: (pattern: RegExp) => Promise; +} { + let text = ''; + const listeners = new Set<() => void>(); + void (async () => { + for await (const chunk of app.stdout) { + text += new TextDecoder().decode(chunk); + listeners.forEach(listener => listener()); + } + })(); + + return { + text: () => text, + waitFor: pattern => + new Promise(resolve => { + const check = (): void => { + const match = text.match(pattern); + if (match) { + listeners.delete(check); + resolve(match); + } + }; + listeners.add(check); + check(); + }), + }; +} diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index c683200dcbeb..817dd32df5d8 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -80,7 +80,7 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "prebuild": "pnpm run build:version", "precheck:types": "pnpm run build:version", "prelint:types": "pnpm run build:version", @@ -89,9 +89,9 @@ "check:types": "run-p check:types:src check:types:test", "check:types:src": "tsc --project ./tsconfig.json --noEmit", "check:types:test": "tsc --project ./test/tsconfig.json --noEmit", - "clean": "rimraf build dist coverage sentry-bundler-plugins-*.tgz", + "clean": "node ../../scripts/rm.mjs build dist coverage sentry-bundler-plugins-*.tgz", "clean:all": "run-p clean clean:deps", - "clean:deps": "rimraf node_modules", + "clean:deps": "node ../../scripts/rm.mjs node_modules", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", "lint:fix": "oxlint . --fix --type-aware", @@ -120,8 +120,7 @@ "glob": "^14.0.3", "magic-string": "~0.30.8 || ^1.0.0", "oxc-parser": "^0.152.0", - "sentry": "^0.45.0", - "supports-color": "^8.1.1" + "sentry": "^0.49.0" }, "peerDependencies": { "rollup": ">=3.2.0", @@ -143,9 +142,9 @@ "@sentry-internal/test-utils": "11.11.0", "@types/babel__core": "^7.20.5", "@types/node": "^20.19.0", - "premove": "^4.0.0", "rolldown": "^1.0.0", - "vitest": "^4.1.11", + "vite": "^8.3.6", + "vitest": "^4.1.12", "webpack": "5.112.2" }, "engines": { diff --git a/packages/bundler-plugins/src/core/build-plugin-manager.ts b/packages/bundler-plugins/src/core/build-plugin-manager.ts index a529629e4413..2e43f8adca09 100644 --- a/packages/bundler-plugins/src/core/build-plugin-manager.ts +++ b/packages/bundler-plugins/src/core/build-plugin-manager.ts @@ -22,6 +22,8 @@ import { import { globFiles } from './glob'; import type { SourceMapDecision, SourceMapKind } from './sourcemap-settings'; import { decideSourceMapHandling } from './sourcemap-settings'; +import type { UploadFilter } from './upload-filter'; +import { createUploadFilter } from './upload-filter'; import { LIB_VERSION } from './version'; // Module-level guard to prevent duplicate deploy records when multiple bundler plugin @@ -53,14 +55,15 @@ export type UploadStaging = { * Stamps the debug ID into the chunk and its source map and writes both into the folder. * * `mapSource` is `undefined` when the chunk has no separate source map. `mapDir` is the directory - * the map would be written to and is passed to the `rewriteSources` hook. + * the map would be written to and is passed to the `rewriteSources` hook. `chunkPath` is where + * the chunk is written; `sourcemaps.assets` and `sourcemaps.ignore` are matched against it. */ stage(input: { code: string; mapSource: string | undefined; mapDir: string; - fileName: string; - }): Promise<'staged' | 'skipped' | 'failed'>; + chunkPath: string; + }): Promise<'staged' | 'skipped' | 'excluded' | 'failed'>; /** Removes the folder. Call this when no upload will consume it. */ discard(): Promise; /** @@ -143,9 +146,17 @@ export type SentryBuildPluginManager = { uploadPossible: boolean; /** * Decides how the plugin treats the user's source map setting (see `decideSourceMapHandling`). - * Logs the decision's message once per message. + * Logs the decision's message once per message, unless `log` is `false`. */ - decide(kind: SourceMapKind): SourceMapDecision; + decide(kind: SourceMapKind, options?: { log?: boolean }): SourceMapDecision; + /** Logs the decision's message, once per message. */ + logDecision(decision: SourceMapDecision): void; + /** + * Whether a file of the build output is uploaded, for files that are not on disk yet. Mirrors + * the glob of the upload from disk: it must match `sourcemaps.assets` (all files when unset) + * and none of `sourcemaps.ignore`. + */ + shouldUpload: UploadFilter; }; /** @@ -237,6 +248,8 @@ export function createSentryBuildPluginManager( sourceMaps: { uploadPossible: false, decide: () => ({ enableHidden: false, deleteAfterUpload: false }), + logDecision: () => undefined, + shouldUpload: () => false, }, deleteArtifacts: async () => { /* noop */ @@ -365,6 +378,14 @@ export function createSentryBuildPluginManager( const sourceMapUploadPossible = getSourceMapUploadBlocker(options, isDevMode) === undefined; const loggedSourceMapMessages = new Set(); + const shouldUpload = createUploadFilter(options); + + function logDecision(decision: SourceMapDecision): void { + if (decision.log && !loggedSourceMapMessages.has(decision.log.message)) { + loggedSourceMapMessages.add(decision.log.message); + logger[decision.log.level](decision.log.message); + } + } /** * Returns a Promise that resolves when all the currently active dependencies are freed again. @@ -872,6 +893,15 @@ export function createSentryBuildPluginManager( }, stagedChunkPaths, async stage(input) { + const chunkPath = path.resolve(input.chunkPath); + const chunkName = path.basename(chunkPath); + if (!shouldUpload(chunkPath)) { + logger.debug( + `Not staging ${chunkName} for upload: excluded by \`sourcemaps.assets\` or \`sourcemaps.ignore\`.`, + ); + return 'excluded'; + } + const result = prepareArtifactForUpload({ code: input.code, mapSource: input.mapSource, @@ -880,7 +910,7 @@ export function createSentryBuildPluginManager( }); if (!result.ok) { - logger.debug(`Not staging ${input.fileName} for upload: ${result.reason}`); + logger.debug(`Not staging ${chunkName} for upload: ${result.reason}`); return 'skipped'; } @@ -888,7 +918,7 @@ export function createSentryBuildPluginManager( try { await writePreparedArtifact(dir, `${result.artifact.debugId}-${index}`, result.artifact); - stagedChunkPaths.add(path.resolve(input.mapDir, path.basename(input.fileName))); + stagedChunkPaths.add(chunkPath); return 'staged'; } catch (e) { sentryScope.captureException('Error staging build artifact for upload'); @@ -920,14 +950,15 @@ export function createSentryBuildPluginManager( sourceMaps: { uploadPossible: sourceMapUploadPossible, - decide(kind) { + decide(kind, { log = true } = {}) { const decision = decideSourceMapHandling(kind, options, sourceMapUploadPossible); - if (decision.log && !loggedSourceMapMessages.has(decision.log.message)) { - loggedSourceMapMessages.add(decision.log.message); - logger[decision.log.level](decision.log.message); + if (log) { + logDecision(decision); } return decision; }, + logDecision, + shouldUpload, }, /** diff --git a/packages/bundler-plugins/src/core/index.ts b/packages/bundler-plugins/src/core/index.ts index e46caaaec203..f19fe4c5642a 100644 --- a/packages/bundler-plugins/src/core/index.ts +++ b/packages/bundler-plugins/src/core/index.ts @@ -114,3 +114,4 @@ export type { SentryBuildPluginManager, UploadStaging } from './build-plugin-man export { createDebugIdUploadFunction, addDebugIdToEmittedArtifacts, stampDebugId } from './debug-id-upload'; export { decideSourceMapHandling } from './sourcemap-settings'; export type { SourceMapDecision, SourceMapKind } from './sourcemap-settings'; +export type { UploadFilter } from './upload-filter'; diff --git a/packages/bundler-plugins/src/core/types.ts b/packages/bundler-plugins/src/core/types.ts index 5712cf3cfa5b..95fca5f937fb 100644 --- a/packages/bundler-plugins/src/core/types.ts +++ b/packages/bundler-plugins/src/core/types.ts @@ -200,6 +200,13 @@ export interface Options { * setting was not set. `true` also removes source maps you enabled yourself. `false` never removes them; the plugin * still turns on hidden source maps when the setting is not set, so they can be uploaded. * + * When the plugin turned source maps on, none of them reach the build output, not even those of chunks that + * `sourcemaps.assets` or `sourcemaps.ignore` keep out of the upload. Source maps you enabled yourself are only + * removed for the JavaScript chunks that were uploaded; excluded chunks keep theirs. + * + * With webpack's array form of `devtool`, `true` replaces the array with one hidden source map setting for + * all asset types, because webpack hides the array from plugins. + * * With Rollup, Vite and webpack the removed source maps are never written to the output directory. With esbuild * they are deleted after the upload. */ diff --git a/packages/bundler-plugins/src/core/upload-filter.ts b/packages/bundler-plugins/src/core/upload-filter.ts new file mode 100644 index 000000000000..dbd1fc5110d2 --- /dev/null +++ b/packages/bundler-plugins/src/core/upload-filter.ts @@ -0,0 +1,65 @@ +import * as path from 'path'; +import { escape as escapeGlob, Ignore } from 'glob'; +import type { NormalizedOptions } from './options-mapping'; + +export type UploadFilter = (filePath: string) => boolean; + +/** + * Mirrors the glob of the upload from disk for files that are not on disk yet: a file is + * uploaded when it matches one of `sourcemaps.assets` (all files when unset) and none of + * `sourcemaps.ignore`. + */ +export function createUploadFilter(options: NormalizedOptions): UploadFilter { + const cwd = process.cwd(); + + const assets = options.sourcemaps?.assets; + // `glob` walks `assets` from the working directory, so a relative pattern is tested against + // the full path. It skips dot files and folders unless a pattern names them. + const matchesAssets = + assets === undefined + ? () => true + : createMatcher( + toArray(assets).map(pattern => toAbsolutePattern(cwd, pattern)), + { cwd, dot: false }, + ); + + // `glob` tests `ignore` against the path relative to the working directory, and it matches + // dot files and folders. + const ignore = toArray(options.sourcemaps?.ignore ?? []); + const matchesIgnore = ignore.length === 0 ? () => false : createMatcher(ignore, { cwd, dot: true }); + + return filePath => matchesAssets(filePath) && !matchesIgnore(filePath); +} + +// `glob` exports the matcher behind its `ignore` option, so both options follow the rules of +// the upload from disk. +function createMatcher(patterns: string[], { cwd, dot }: { cwd: string; dot: boolean }): UploadFilter { + const matcher = new Ignore([], { nocase: process.platform === 'darwin' || process.platform === 'win32' }); + // `Ignore` always parses in `dot: true` mode, so the option is set before the patterns are added. + matcher.mmopts.dot = dot; + for (const pattern of patterns) { + matcher.add(pattern); + } + return filePath => { + const fullpath = path.resolve(cwd, filePath); + const relative = toGlobRelativePath(cwd, fullpath); + return matcher.ignored({ fullpath: () => fullpath, relative: () => relative } as Parameters[0]); + }; +} + +function toAbsolutePattern(cwd: string, pattern: string): string { + return path.isAbsolute(pattern) ? pattern : `${escapeGlob(cwd.split(path.sep).join('/'))}/${pattern}`; +} + +// Like path-scurry: a file outside the working directory gets a `../` path, unless the two +// share only the file system root. Then it keeps its full path. +function toGlobRelativePath(cwd: string, fullpath: string): string { + const relative = path.relative(cwd, fullpath); + const levelsUp = relative.split(path.sep).filter(part => part === '..').length; + const sharedFolder = path.resolve(cwd, ...new Array(levelsUp).fill('..')); + return levelsUp > 0 && sharedFolder === path.parse(cwd).root ? fullpath : relative; +} + +function toArray(patterns: string | string[]): string[] { + return Array.isArray(patterns) ? patterns : [patterns]; +} diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts index b369bc67a3ec..136b4a20665a 100644 --- a/packages/bundler-plugins/src/esbuild/index.ts +++ b/packages/bundler-plugins/src/esbuild/index.ts @@ -55,6 +55,7 @@ interface EsbuildInitialOptions { define?: Record; write?: boolean; absWorkingDir?: string; + logOverride?: Record; } interface EsbuildPluginBuild { @@ -199,6 +200,10 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any { // rebuild wraps the entry points again. onStart(() => debugIdWrappedPaths.clear()); + // The proxy module re-exports `OriginalModule.default` statically so downstream bundlers can tree-shake it, + // but esbuild warns about that access when the entry point has no default export. + initialOptions.logOverride = { 'import-is-undefined': 'silent', ...initialOptions.logOverride }; + if (!initialOptions.bundle) { logger.warn( 'The Sentry esbuild plugin only supports esbuild with `bundle: true` being set in the esbuild build options. Esbuild will probably crash now. Sorry about that. If you need to upload sourcemaps without `bundle: true`, it is recommended to use Sentry CLI instead: https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/cli/', diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index 227f162eae8f..7017577c6919 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -21,6 +21,8 @@ import * as path from 'node:path'; import { createRequire } from 'node:module'; import { finalizeRolldownDebugIds, getDebugIdForChunk, hasExistingDebugID } from './debug-id-injection'; import { stampDebugIds, type OutputBundle } from './debug-id-stamping'; +import { createSourceMapHandling, type OutputDestinationOptions } from './source-map-handling'; +import { createViteSourceMapHooks } from '../vite/source-map-hooks'; // The subset of Rollup's `TransformResult` that this plugin's `transform` // hook actually returns. Defined locally instead of imported from `rollup` @@ -81,10 +83,14 @@ function getViteParseAstAsync(): Promise { } /** - * @ignore - this is the internal plugin factory function only used for the Vite plugin! + * @ignore - internal plugin factory shared by the Rollup and Vite plugins. + * + * `main` carries injection, release creation and the upload. `post` runs last in `generateBundle`, + * copies chunks and their source maps into the upload staging folder and drops the map assets + * from the bundle, so the bundler never writes them. */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function _rollupPluginInternal( +export function _createRollupPluginInternals( userOptions: Options = {}, buildTool: 'rollup' | 'vite', buildToolMajorVersion?: string, @@ -105,7 +111,10 @@ export function _rollupPluginInternal( if (options.disable) { return { - name: 'sentry-noop-plugin', + main: { + name: 'sentry-noop-plugin', + }, + post: undefined, }; } @@ -116,6 +125,7 @@ export function _rollupPluginInternal( const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); const sourcemapsEnabled = options.sourcemaps?.disable !== true; + const sourceMapHandling = createSourceMapHandling(sentryBuildPluginManager); const releaseInjectionCode = new CodeInjection(); const chunkInjectionCode = new CodeInjection(); @@ -255,14 +265,15 @@ export function _rollupPluginInternal( stampDebugIds(bundle, true); } - async function writeBundle( - outputOptions: { dir?: string; file?: string }, - bundle: { [fileName: string]: unknown }, - ): Promise { + async function writeBundle(outputOptions: OutputDestinationOptions, bundle: OutputBundle | undefined): Promise { + const staging = await sourceMapHandling.takeStaging(outputOptions, bundle); + let stagingHandedOver = false; + try { await sentryBuildPluginManager.createRelease(); if (sourcemapsEnabled && options.sourcemaps?.disable !== 'disable-upload') { + let buildArtifacts: string[]; if (outputOptions.dir) { const outputDir = outputOptions.dir; const JS_AND_MAP_PATTERNS = [ @@ -273,15 +284,23 @@ export function _rollupPluginInternal( '/**/*.mjs.map', '/**/*.cjs.map', ].map(q => `${q}?(\\?*)?(#*)`); // We want to allow query and hash strings at the end of files - const buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir }); - await upload(buildArtifacts); + buildArtifacts = await globFiles(JS_AND_MAP_PATTERNS, { root: outputDir }); } else if (outputOptions.file) { - await upload([outputOptions.file]); + buildArtifacts = [outputOptions.file]; } else { - const buildArtifacts = Object.keys(bundle).map(asset => path.join(path.resolve(), asset)); - await upload(buildArtifacts); + buildArtifacts = Object.keys(bundle ?? {}).map(asset => path.join(path.resolve(), asset)); } + stagingHandedOver = true; + await upload(buildArtifacts, { staging }); + } else { + stagingHandedOver = true; + await staging?.discard(); } + } catch (e) { + if (!stagingHandedOver) { + await staging?.keep('The source map upload did not run.'); + } + throw e; } finally { freeGlobalDependencyOnBuildArtifacts(); await sentryBuildPluginManager.deleteArtifacts(); @@ -297,21 +316,64 @@ export function _rollupPluginInternal( } : transform; - return { + // Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin + // decides in a `config` hook instead. + const viteSourceMapHooks = + buildTool === 'vite' ? createViteSourceMapHooks(sentryBuildPluginManager, sourceMapHandling) : undefined; + + const main = { name, buildStart, ...(shouldTransform ? { transform: transformHook } : {}), + ...(viteSourceMapHooks + ? { configResolved: viteSourceMapHooks.configResolved } + : { outputOptions: sourceMapHandling.outputOptions }), renderChunk, generateBundle: { order: 'pre' as const, handler: generateBundle }, writeBundle, }; + + const post = { + name: buildTool === 'vite' ? 'sentry-vite-upload-staging' : 'sentry-rollup-upload-staging', + // On the post plugin, so framework plugins that set `build.sourcemap` + // themselves run first and the decision sees the merged setting. + ...(viteSourceMapHooks ? { config: viteSourceMapHooks.config } : {}), + generateBundle: { + order: 'post' as const, + async handler(outputOptions: OutputDestinationOptions, bundle: OutputBundle, isWrite?: boolean): Promise { + if (buildTool === 'vite' && options.sourcemaps?.disable === 'disable-upload') { + // Vite's module preload rewrite in `generateBundle` regenerates the source map of every + // chunk with a dynamic import, which drops the debug ID the main plugin stamped in its + // `pre` hook. Restore it without touching the chunk code, so file hashes stay valid. + stampDebugIds(bundle, false); + } + + await sourceMapHandling.stageSourceMaps(outputOptions, bundle, isWrite); + }, + }, + }; + + return { main, post }; +} + +/** + * @ignore - this is the internal plugin factory function only used for the Vite plugin! + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function _rollupPluginInternal( + userOptions: Options = {}, + buildTool: 'rollup' | 'vite', + buildToolMajorVersion?: string, +) { + return _createRollupPluginInternals(userOptions, buildTool, buildToolMajorVersion).main; } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any export function sentryRollupPlugin(userOptions: Options = {}): any { + const { main, post } = _createRollupPluginInternals(userOptions, 'rollup'); // We return an array here so we don't break backwards compatibility with what // unplugin used to return - return [_rollupPluginInternal(userOptions, 'rollup')]; + return post ? [main, post] : [main]; } export type { Options as SentryRollupPluginOptions } from '../core'; diff --git a/packages/bundler-plugins/src/rollup/source-map-handling.ts b/packages/bundler-plugins/src/rollup/source-map-handling.ts new file mode 100644 index 000000000000..b907b217afed --- /dev/null +++ b/packages/bundler-plugins/src/rollup/source-map-handling.ts @@ -0,0 +1,288 @@ +import * as path from 'node:path'; +import type { SentryBuildPluginManager, SourceMapDecision, SourceMapKind, UploadStaging } from '../core'; +import { isJsFile } from '../core'; +import type { OutputBundle } from './debug-id-stamping'; + +export type RollupSourceMapSetting = boolean | 'inline' | 'hidden' | undefined; + +export type OutputDestinationOptions = { dir?: string; file?: string }; + +/** + * The output options that tell outputs of one directory apart. Several outputs can write into the + * same directory at once, for example an ES and a CJS build, each with its own `sourcemap` setting. + * `sourcemap` holds the setting as the plugin left it, so an output it switched to hidden maps + * differs from one the user set. + */ +export type OutputMatch = { + dir?: unknown; + file?: unknown; + format?: unknown; + entryFileNames?: unknown; + chunkFileNames?: unknown; + preserveModules?: unknown; + sourcemap?: unknown; +}; + +export type OutputOptions = OutputDestinationOptions & OutputMatch & { sourcemap?: RollupSourceMapSetting }; + +/** Maps Rollup's (and Vite's) `sourcemap` setting onto the plugin's source map kinds. */ +export function sourceMapKindFromRollupSetting(setting: RollupSourceMapSetting): SourceMapKind { + switch (setting) { + case undefined: + return 'unset'; + case false: + return 'off'; + case true: + return 'visible'; + default: + return setting; + } +} + +/** The directory an output writes to. Decisions and stagings are keyed by it. */ +export function getOutputDestination(outputOptions: OutputDestinationOptions): string { + return path.resolve(outputOptions.dir ?? path.dirname(outputOptions.file ?? '.')); +} + +const MATCH_KEYS = [ + 'dir', + 'file', + 'format', + 'entryFileNames', + 'chunkFileNames', + 'preserveModules', + 'sourcemap', +] as const; + +/** The raw options accept aliases that the normalized options do not carry. */ +function normalizeFormat(format: unknown): unknown { + switch (format) { + case 'esm': + case 'module': + return 'es'; + case 'commonjs': + return 'cjs'; + case 'systemjs': + return 'system'; + default: + return format; + } +} + +/** The fields of `OutputMatch` an output sets, in the form the normalized options carry them. */ +export function getOutputMatch(outputOptions: OutputMatch): OutputMatch { + const match: OutputMatch = {}; + for (const key of MATCH_KEYS) { + const value = outputOptions[key]; + if (value === undefined) { + continue; + } + match[key] = + key === 'format' ? normalizeFormat(value) : key === 'dir' || key === 'file' ? path.resolve(String(value)) : value; + } + return match; +} + +function matchesOutput(match: OutputMatch, outputOptions: OutputMatch): boolean { + const actual = getOutputMatch(outputOptions); + return MATCH_KEYS.every(key => match[key] === undefined || match[key] === actual[key]); +} + +function isSameMatch(a: OutputMatch, b: OutputMatch): boolean { + return MATCH_KEYS.every(key => a[key] === b[key]); +} + +function countMatchFields(match: OutputMatch): number { + return MATCH_KEYS.filter(key => match[key] !== undefined).length; +} + +const DEBUG_ID_PATTERN = /sentry-dbid-([0-9a-f-]{36})|\/\/# debugId=([0-9a-f-]{36})/i; + +/** Identifies the chunks of a bundle by name and debug ID, so a staging can be matched to its write. */ +function getBundleSignature(bundle: OutputBundle): string { + const entries: string[] = []; + for (const output of Object.values(bundle)) { + if (output.type === 'chunk' && isJsFile(output.fileName)) { + const match = output.code.match(DEBUG_ID_PATTERN); + entries.push(`${output.fileName}:${match?.[1] ?? match?.[2] ?? ''}`); + } + } + return entries.sort().join('\n'); +} + +function getSourceMapFileName(output: { fileName: string; sourcemapFileName?: string | null }): string { + return output.sourcemapFileName ?? `${output.fileName}.map`; +} + +type PendingStaging = { staging: UploadStaging; signature: string; match: OutputMatch }; + +export type DecisionRecord = { match: OutputMatch; kind: SourceMapKind; decision: SourceMapDecision }; + +export type SourceMapHandling = { + /** Rollup's `outputOptions` hook: decides for the output and switches it to hidden source maps when needed. */ + outputOptions: (rawOutputOptions: OutputOptions) => OutputOptions | null; + /** Records the decision for an output, for bundlers that decide outside `outputOptions`. */ + setDecision: (destination: string, record: DecisionRecord) => void; + /** + * `generateBundle` with `order: 'post'`: copies each chunk and its source map into a staging folder + * and drops the map asset from the bundle, so the bundler never writes it. + */ + stageSourceMaps: (outputOptions: OutputOptions, bundle: OutputBundle, isWrite?: boolean) => Promise; + /** + * Hands the staging of this bundle to `writeBundle`. Without a bundle (SvelteKit calls the hook + * with output options only) the oldest staging is taken. + */ + takeStaging: ( + outputOptions: OutputDestinationOptions, + bundle: OutputBundle | undefined, + ) => Promise; +}; + +/** + * Source map decisions and upload stagings of one plugin instance. Keyed by output destination + * because one instance can serve several outputs, and watch mode or several `write()` calls + * rebuild the same one. Outputs that share a directory are told apart by `OutputMatch`. + */ +export function createSourceMapHandling(sentryBuildPluginManager: SentryBuildPluginManager): SourceMapHandling { + const decisions = new Map(); + const pendingStagings = new Map(); + + function setDecision(destination: string, record: DecisionRecord): void { + const records = decisions.get(destination) ?? []; + const index = records.findIndex(existing => isSameMatch(existing.match, record.match)); + if (index === -1) { + records.push(record); + } else { + records[index] = record; + } + decisions.set(destination, records); + } + + function findDecision(destination: string, outputOptions: OutputOptions): DecisionRecord | undefined { + const candidates = (decisions.get(destination) ?? []).filter(record => matchesOutput(record.match, outputOptions)); + // The record that sets the most fields describes this output best. Equal + // candidates keep their order, so the first one recorded wins. + return candidates.sort((a, b) => countMatchFields(b.match) - countMatchFields(a.match))[0]; + } + + function dropSourceMaps(bundle: OutputBundle): void { + for (const output of Object.values(bundle)) { + if (output.type === 'chunk' && isJsFile(output.fileName)) { + // oxlint-disable-next-line typescript/no-dynamic-delete -- Rollup drops an asset when its key is deleted from the bundle + delete bundle[getSourceMapFileName(output)]; + } + } + } + + return { + outputOptions: rawOutputOptions => { + const kind = sourceMapKindFromRollupSetting(rawOutputOptions.sourcemap); + // Logged in `generateBundle`, which knows whether the output is written. + const decision = sentryBuildPluginManager.sourceMaps.decide(kind, { log: false }); + const outputOptions = decision.enableHidden + ? { ...rawOutputOptions, sourcemap: 'hidden' as const } + : rawOutputOptions; + setDecision(getOutputDestination(rawOutputOptions), { match: getOutputMatch(outputOptions), kind, decision }); + + return decision.enableHidden ? outputOptions : null; + }, + + setDecision, + + stageSourceMaps: async (outputOptions, bundle, isWrite) => { + const destination = getOutputDestination(outputOptions); + const record = findDecision(destination, outputOptions); + if (!record) { + return; + } + + // `generate()` never reaches `writeBundle`, so nothing would upload a staging. Maps the + // plugin turned on are dropped, so the result holds what the user configured. + if (isWrite === false) { + if (record.kind === 'unset' && record.decision.enableHidden) { + dropSourceMaps(bundle); + } + return; + } + + sentryBuildPluginManager.sourceMaps.logDecision(record.decision); + if (!record.decision.deleteAfterUpload) { + return; + } + + const staging = await sentryBuildPluginManager.createUploadStaging(); + + try { + for (const output of Object.values(bundle)) { + if (output.type !== 'chunk' || !isJsFile(output.fileName)) { + continue; + } + + const mapFileName = getSourceMapFileName(output); + const mapAsset = bundle[mapFileName]; + if (mapAsset?.type !== 'asset' || typeof mapAsset.source !== 'string') { + continue; + } + + const result = await staging.stage({ + code: output.code, + mapSource: mapAsset.source, + mapDir: path.join(destination, path.dirname(mapFileName)), + chunkPath: path.join(destination, output.fileName), + }); + + // A map the plugin turned on never reaches the output, whether or + // not the chunk is uploaded. A map the user enabled stays unless + // it was staged. + if (result === 'staged' || record.kind === 'unset') { + // oxlint-disable-next-line typescript/no-dynamic-delete -- Rollup drops an asset when its key is deleted from the bundle + delete bundle[mapFileName]; + } + if (result === 'skipped' || result === 'failed') { + sentryBuildPluginManager.logger.warn( + record.kind === 'unset' + ? `Could not prepare ${output.fileName} for upload, so it is not uploaded and its source map ${mapFileName} was removed from the build output.` + : `Could not prepare ${output.fileName} for upload, so its source map ${mapFileName} stays in the build output.`, + ); + } + } + } catch (e) { + // The error fails the build before anything is written. + await staging.discard(); + throw e; + } + + // A staging of the same output that no `writeBundle` took belongs to a + // write that never finished. Stagings of other outputs to this directory + // may still be in flight, so the shared list changes in place and without + // an await in between. + const stagings = pendingStagings.get(destination) ?? []; + pendingStagings.set(destination, stagings); + const superseded = stagings.filter(pending => isSameMatch(pending.match, record.match)); + for (const pending of superseded) { + stagings.splice(stagings.indexOf(pending), 1); + } + stagings.push({ staging, signature: getBundleSignature(bundle), match: record.match }); + + for (const pending of superseded) { + sentryBuildPluginManager.logger.debug( + `Discarding staged source maps of an earlier build of ${destination} that was never written.`, + ); + await pending.staging.discard(); + } + }, + + takeStaging: async (outputOptions, bundle) => { + const destination = getOutputDestination(outputOptions); + const stagings = pendingStagings.get(destination) ?? []; + + if (!bundle) { + return stagings.shift()?.staging; + } + + const signature = getBundleSignature(bundle); + const index = stagings.findIndex(pending => pending.signature === signature); + return index === -1 ? undefined : stagings.splice(index, 1)[0]?.staging; + }, + }; +} diff --git a/packages/bundler-plugins/src/vite/index.ts b/packages/bundler-plugins/src/vite/index.ts index d9b06efd9c25..9c8d5437fe03 100644 --- a/packages/bundler-plugins/src/vite/index.ts +++ b/packages/bundler-plugins/src/vite/index.ts @@ -1,15 +1,11 @@ import type { SentryRollupPluginOptions } from '../rollup'; -import { _rollupPluginInternal } from '../rollup'; +import { _createRollupPluginInternals } from '../rollup'; import { createRequire } from 'node:module'; -import { stampDebugIds, type OutputBundle } from '../rollup/debug-id-stamping'; interface SentryVitePlugin { name: string; enforce: 'pre' | 'post'; - generateBundle?: { - order: 'pre' | 'post'; - handler: (_outputOptions: unknown, bundle: OutputBundle) => void; - }; + [hook: string]: unknown; } function getViteMajorVersion(): string | undefined { @@ -27,27 +23,12 @@ function getViteMajorVersion(): string | undefined { } export const sentryVitePlugin = (options?: SentryRollupPluginOptions): SentryVitePlugin[] => { - const plugins: SentryVitePlugin[] = [ - { - enforce: 'pre', - ..._rollupPluginInternal(options, 'vite', getViteMajorVersion()), - }, - ]; - - if (!options?.disable && options?.sourcemaps?.disable === 'disable-upload') { - plugins.push({ - name: 'sentry-vite-debug-id-sourcemaps', - enforce: 'post', - generateBundle: { - order: 'post', - handler(_outputOptions, bundle) { - // Vite's module preload rewrite in `generateBundle` regenerates the source map of every - // chunk with a dynamic import, which drops the debug ID the main plugin stamped in its - // `pre` hook. Restore it without touching the chunk code, so file hashes stay valid. - stampDebugIds(bundle, false); - }, - }, - }); + const { main, post } = _createRollupPluginInternals(options, 'vite', getViteMajorVersion()); + + const plugins: SentryVitePlugin[] = [{ enforce: 'pre', ...main }]; + + if (post) { + plugins.push({ enforce: 'post', ...post }); } return plugins; diff --git a/packages/bundler-plugins/src/vite/source-map-hooks.ts b/packages/bundler-plugins/src/vite/source-map-hooks.ts new file mode 100644 index 000000000000..04e3eedc179a --- /dev/null +++ b/packages/bundler-plugins/src/vite/source-map-hooks.ts @@ -0,0 +1,147 @@ +import * as path from 'node:path'; +import type { SentryBuildPluginManager, SourceMapDecision, SourceMapKind } from '../core'; +import type { OutputMatch, RollupSourceMapSetting, SourceMapHandling } from '../rollup/source-map-handling'; +import { getOutputMatch, sourceMapKindFromRollupSetting } from '../rollup/source-map-handling'; + +type BundlerOutput = OutputMatch & { dir?: string; sourcemap?: RollupSourceMapSetting }; +type BundlerOutputOptions = BundlerOutput | BundlerOutput[] | undefined; + +type ViteBuildOptions = { + sourcemap?: RollupSourceMapSetting; + outDir?: string; + rollupOptions?: { output?: BundlerOutputOptions }; + rolldownOptions?: { output?: BundlerOutputOptions }; +}; + +export type ViteUserConfig = { + build?: ViteBuildOptions; + environments?: Record; +}; + +export type ViteResolvedConfig = { + root: string; + build: ViteBuildOptions; + environments?: Record; +}; + +export type ViteSourceMapHooks = { + config: (userConfig: ViteUserConfig, env: { command: string }) => ViteUserConfig | null; + configResolved: (config: ViteResolvedConfig) => void; +}; + +const NO_CHANGE: SourceMapDecision = { enableHidden: false, deleteAfterUpload: false }; + +type EnvironmentOutput = { + destination: string; + match: OutputMatch; + sourcemap: RollupSourceMapSetting; + /** The output sets `sourcemap` itself, which overrides `build.sourcemap`. */ + ownSetting: boolean; +}; + +/** + * The outputs an environment writes: one per entry of `rollupOptions.output` or + * `rolldownOptions.output`. An output writes to `build.outDir` (relative to the root) unless it + * sets its own `dir`, which the bundler resolves from the working directory. An output's own + * `sourcemap` overrides `build.sourcemap`. + */ +function getOutputs(root: string, build: ViteBuildOptions): EnvironmentOutput[] { + const configured = build.rolldownOptions?.output ?? build.rollupOptions?.output; + const outputs = Array.isArray(configured) ? configured : [configured ?? {}]; + + return outputs.map(output => { + const sourcemap = output.sourcemap ?? build.sourcemap; + return { + destination: output.dir === undefined ? path.resolve(root, build.outDir ?? 'dist') : path.resolve(output.dir), + match: getOutputMatch({ ...output, sourcemap }), + sourcemap, + ownSetting: output.sourcemap !== undefined, + }; + }); +} + +/** + * Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin decides on `build.sourcemap` + * in a `config` hook. The hook belongs on the post plugin: framework plugins that set + * `build.sourcemap` themselves run first, and the decision sees the merged setting. The output + * directories are only known in `configResolved`, which then records the decision for each of them. + */ +export function createViteSourceMapHooks( + sentryBuildPluginManager: SentryBuildPluginManager, + sourceMapHandling: SourceMapHandling, +): ViteSourceMapHooks { + // `'*'` for the top-level setting, otherwise the name of an environment with its own. + const decisions = new Map(); + + return { + config: (userConfig, env) => { + if (env.command !== 'build') { + return null; + } + + // The same plugin instances can serve several builds in a row. + decisions.clear(); + + const kind = sourceMapKindFromRollupSetting(userConfig.build?.sourcemap); + // Logged in `generateBundle`, which knows whether the output is written. + const decision = sentryBuildPluginManager.sourceMaps.decide(kind, { log: false }); + decisions.set('*', { kind, decision }); + + const result: ViteUserConfig = decision.enableHidden ? { build: { sourcemap: 'hidden' } } : {}; + + for (const [name, environment] of Object.entries(userConfig.environments ?? {})) { + const setting = environment?.build?.sourcemap; + if (setting === undefined) { + continue; + } + + const environmentKind = sourceMapKindFromRollupSetting(setting); + const environmentDecision = sentryBuildPluginManager.sourceMaps.decide(environmentKind, { log: false }); + decisions.set(name, { kind: environmentKind, decision: environmentDecision }); + + if (environmentDecision.enableHidden) { + result.environments = { ...result.environments, [name]: { build: { sourcemap: 'hidden' } } }; + } + } + + return result; + }, + + configResolved: config => { + const environments = config.environments ?? { client: { build: config.build } }; + + const { uploadPossible } = sentryBuildPluginManager.sourceMaps; + const deleteOption = sentryBuildPluginManager.normalizedOptions.sourcemaps?.deleteSourcemapsAfterUpload; + + for (const [name, environment] of Object.entries(environments)) { + const { kind, decision } = decisions.get(name) ?? decisions.get('*') ?? { kind: 'unset', decision: NO_CHANGE }; + + for (const output of getOutputs(config.root, environment.build)) { + // The plugin never changes an output's own setting, so the user enabled + // these maps: only hidden ones the user asked to delete are removed. + if (output.ownSetting) { + sourceMapHandling.setDecision(output.destination, { + match: output.match, + kind: sourceMapKindFromRollupSetting(output.sourcemap), + decision: { + enableHidden: false, + deleteAfterUpload: uploadPossible && deleteOption === true && output.sourcemap === 'hidden', + }, + }); + continue; + } + + // A plugin that ran after ours may have changed the setting again. + if (decision.deleteAfterUpload && output.sourcemap !== 'hidden') { + sentryBuildPluginManager.logger.debug( + `Source maps of the "${name}" environment are set to ${JSON.stringify(output.sourcemap)}. Not removing them after upload.`, + ); + sourceMapHandling.setDecision(output.destination, { match: output.match, kind, decision: NO_CHANGE }); + } else { + sourceMapHandling.setDecision(output.destination, { match: output.match, kind, decision }); + } + } + } + }, + }; +} diff --git a/packages/bundler-plugins/src/webpack/index.ts b/packages/bundler-plugins/src/webpack/index.ts index 84600ff47634..85795477405d 100644 --- a/packages/bundler-plugins/src/webpack/index.ts +++ b/packages/bundler-plugins/src/webpack/index.ts @@ -1,6 +1,6 @@ /* oxlint-disable max-lines */ -import type { Options } from '../core/index'; +import type { Options, SourceMapDecision, SourceMapKind, UploadStaging } from '../core/index'; import { createSentryBuildPluginManager, generateReleaseInjectorCode, @@ -71,6 +71,7 @@ type WebpackCompiler = { options: { plugins?: unknown[]; mode?: string; + devtool?: string | false; module?: { rules?: unknown[]; }; @@ -82,18 +83,25 @@ type WebpackCompiler = { thisCompilation: { tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void; }; + emit: { + tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void; + }; afterEmit: { tapAsync: (name: string, callback: (compilation: WebpackCompilation, cb: () => void) => void) => void; }; done: { tap: (name: string, callback: () => void) => void; }; + failed?: { + tap: (name: string, callback: () => void) => void; + }; }; webpack?: { DefinePlugin?: PluginClass; Compilation?: { PROCESS_ASSETS_STAGE_ADDITIONS?: number; PROCESS_ASSETS_STAGE_DEV_TOOLING?: number; + PROCESS_ASSETS_STAGE_REPORT?: number; }; sources?: { RawSource?: WebpackRawSource; @@ -116,16 +124,48 @@ type WebpackCompilation = { getAssets: () => WebpackAsset[]; getAsset: (name: string) => WebpackAsset | undefined; updateAsset: (name: string, source: WebpackSource) => void; + deleteAsset: (name: string) => void; hooks: { processAssets: { tap: ( options: { name: string; stage: number }, callback: (assets: Record) => void, ) => void; + tapPromise: (options: { name: string; stage: number }, callback: () => Promise) => void; }; }; }; +/** + * Maps webpack's `devtool` onto the plugin's source map kinds. A user who sets `devtool: false` + * and adds a `SourceMapDevToolPlugin` manages source maps themselves. + */ +function sourceMapKindFromDevtool(compiler: WebpackCompiler): SourceMapKind | undefined { + const devtool = compiler.options.devtool; + + if (devtool === undefined) { + return 'unset'; + } + if (devtool === false) { + const hasSourceMapPlugin = compiler.options.plugins?.some( + plugin => + (plugin as { constructor?: { name?: string } } | undefined)?.constructor?.name === 'SourceMapDevToolPlugin', + ); + return hasSourceMapPlugin ? undefined : 'off'; + } + // Plain `eval` has no source maps at all. + if (!devtool.includes('source-map')) { + return 'off'; + } + if (devtool.includes('inline') || devtool.includes('eval')) { + return 'inline'; + } + if (devtool.includes('hidden')) { + return 'hidden'; + } + return 'visible'; +} + type WebpackModule = { version?: string; default?: { version?: string }; @@ -171,6 +211,66 @@ function addDebugIdsToAssets(compilation: WebpackCompilation, RawSource: Webpack } } +/** Drops a source map asset and its compressed variants from the compilation. */ +function deleteSourceMapAssets(compilation: WebpackCompilation, sourceMapName: string): void { + for (const candidate of compilation.getAssets()) { + if (candidate.name === sourceMapName || candidate.name.startsWith(`${sourceMapName}.`)) { + compilation.deleteAsset(candidate.name); + } + } +} + +/** + * Copies each JS asset and its source map into the upload staging folder and drops the source map + * assets (and their compressed variants) from the compilation, so webpack never emits them. + * + * A map the plugin turned on (`pluginEnabledMaps`) never reaches the output, whether or not its + * asset is uploaded. That covers CSS maps, which `devtool` turns on along with the JS ones. A map + * the user enabled stays unless it was staged. + */ +async function stageSourceMaps( + compilation: WebpackCompilation, + staging: UploadStaging, + logger: { warn: (message: string) => void }, + pluginEnabledMaps: boolean, +): Promise { + const outputPath = compilation.outputOptions.path ?? path.resolve(); + + for (const asset of compilation.getAssets()) { + const relatedSourceMap = asset.info.related?.sourceMap; + const sourceMapName = typeof relatedSourceMap === 'string' ? relatedSourceMap : `${asset.name}.map`; + const sourceMapAsset = compilation.getAsset(sourceMapName); + if (!sourceMapAsset) { + continue; + } + + if (!isJsFile(asset.name)) { + if (pluginEnabledMaps) { + deleteSourceMapAssets(compilation, sourceMapName); + } + continue; + } + + const result = await staging.stage({ + code: asset.source.source().toString(), + mapSource: sourceMapAsset.source.source().toString(), + mapDir: path.join(outputPath, path.dirname(sourceMapName)), + chunkPath: path.join(outputPath, asset.name), + }); + + if (result === 'skipped' || result === 'failed') { + logger.warn( + pluginEnabledMaps + ? `Could not prepare ${asset.name} for upload, so it is not uploaded and its source map ${sourceMapName} was removed from the build output.` + : `Could not prepare ${asset.name} for upload, so its source map ${sourceMapName} stays in the build output.`, + ); + } + if (result === 'staged' || pluginEnabledMaps) { + deleteSourceMapAssets(compilation, sourceMapName); + } + } +} + function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) { const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? '[sentry-webpack-plugin]', @@ -231,6 +331,21 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) const transformReplace = Object.keys(replacementValues).length > 0; + function decideSourceMaps(compiler: WebpackCompiler): { kind?: SourceMapKind; decision: SourceMapDecision } { + const kind = compiler.options.mode === 'development' ? undefined : sourceMapKindFromDevtool(compiler); + if (kind === undefined) { + return { decision: { enableHidden: false, deleteAfterUpload: false } }; + } + + const decision = sentryBuildPluginManager.sourceMaps.decide(kind); + + if (decision.enableHidden) { + compiler.options.devtool = kind === 'visible' ? `hidden-${compiler.options.devtool}` : 'hidden-source-map'; + } + + return { kind, decision }; + } + function addCodeInjection(compiler: WebpackCompiler): void { if (releaseInjectionCode.isEmpty() && chunkInjectionCode.isEmpty() && !sourcemapsEnabled) { return; @@ -299,6 +414,47 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) const { DefinePlugin } = compiler.webpack ?? {}; + // One plugin instance can serve several compilers (`MultiCompiler`) and + // a compiler several compilations (watch mode), so nothing is shared. + const { kind: sourceMapKind, decision: sourceMapDecision } = decideSourceMaps(compiler); + const pendingStagings = new Map(); + + if (sourceMapDecision.deleteAfterUpload) { + // After `RealContentHashPlugin` rewrote the hash placeholders, so the staged + // JS equals the emitted file and `[contenthash]` names are final. + const stage = (compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_REPORT ?? 5000) + 1; + + compiler.hooks.thisCompilation.tap('sentry-webpack-plugin', compilation => { + compilation.hooks.processAssets.tapPromise({ name: 'sentry-webpack-plugin', stage }, async () => { + const staging = await sentryBuildPluginManager.createUploadStaging(); + pendingStagings.set(compilation, staging); + await stageSourceMaps(compilation, staging, logger, sourceMapKind === 'unset'); + }); + }); + + // A staging that `afterEmit` never took belongs to a compilation that + // failed before emitting, or to one whose assets were emitted without + // their maps because an earlier `afterEmit` tap failed. Only the + // second kind has no other copy of the maps. + const emittedCompilations = new Set(); + compiler.hooks.emit.tap('sentry-webpack-plugin', compilation => { + emittedCompilations.add(compilation); + }); + const settlePendingStagings = (): void => { + for (const [compilation, staging] of pendingStagings) { + if (emittedCompilations.has(compilation)) { + void staging.keep('The source map upload did not run.'); + } else { + void staging.discard(); + } + } + pendingStagings.clear(); + emittedCompilations.clear(); + }; + compiler.hooks.done.tap('sentry-webpack-plugin', settlePendingStagings); + compiler.hooks.failed?.tap('sentry-webpack-plugin', settlePendingStagings); + } + // BannerPlugin prepends executable code before directive prologues. addCodeInjection(compiler); @@ -354,14 +510,27 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); + const staging = pendingStagings.get(compilation); + pendingStagings.delete(compilation); + let stagingHandedOver = false; + const run = async (): Promise => { try { await sentryBuildPluginManager.createRelease(); if (sourcemapsEnabled && options.sourcemaps?.disable !== 'disable-upload') { const outputPath = compilation.outputOptions.path ?? path.resolve(); const buildArtifacts = Object.keys(compilation.assets).map(asset => path.join(outputPath, asset)); - await upload(buildArtifacts); + stagingHandedOver = true; + await upload(buildArtifacts, { staging }); + } else { + stagingHandedOver = true; + await staging?.discard(); + } + } catch (e) { + if (!stagingHandedOver) { + await staging?.keep('The source map upload did not run.'); } + throw e; } finally { freeGlobalDependencyOnBuildArtifacts(); await sentryBuildPluginManager.deleteArtifacts(); diff --git a/packages/bundler-plugins/test/core/upload-filter.test.ts b/packages/bundler-plugins/test/core/upload-filter.test.ts new file mode 100644 index 000000000000..57c4bcc821f2 --- /dev/null +++ b/packages/bundler-plugins/test/core/upload-filter.test.ts @@ -0,0 +1,127 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { escape as escapeGlob } from 'glob'; +import { afterAll, beforeAll, describe, it, expect, vi } from 'vitest'; +import { globFiles } from '../../src/core/glob'; +import { createUploadFilter } from '../../src/core/upload-filter'; +import { normalizeUserOptions } from '../../src/core/options-mapping'; + +function filter(sourcemaps: { assets?: string | string[]; ignore?: string | string[] }) { + return createUploadFilter(normalizeUserOptions({ sourcemaps })); +} + +const inDist = path.resolve('dist/app/main.js'); +const vendor = path.resolve('dist/app/vendor-abc.js'); +const elsewhere = path.resolve('other/main.js'); + +describe('createUploadFilter', () => { + it('accepts every file when neither option is set', () => { + expect(filter({})(inDist)).toBe(true); + expect(filter({})(elsewhere)).toBe(true); + }); + + it('accepts only files matching `assets`, resolved against the working directory', () => { + const shouldUpload = filter({ assets: 'dist/**' }); + expect(shouldUpload(inDist)).toBe(true); + expect(shouldUpload('dist/app/main.js')).toBe(true); + expect(shouldUpload(elsewhere)).toBe(false); + }); + + it('accepts files matching any of several `assets` patterns', () => { + const shouldUpload = filter({ assets: ['other/*.js', path.resolve('dist/**/*.js')] }); + expect(shouldUpload(inDist)).toBe(true); + expect(shouldUpload(elsewhere)).toBe(true); + expect(shouldUpload(path.resolve('dist/app/styles.css'))).toBe(false); + }); + + it('rejects everything for an empty `assets` list', () => { + expect(filter({ assets: [] })(inDist)).toBe(false); + }); + + it('matches `**` patterns against files outside the working directory, like `glob`', () => { + const outside = path.join(path.parse(process.cwd()).root, 'tmp', 'build', 'vendor-abc.js'); + expect(filter({ ignore: '**/vendor-*.js' })(outside)).toBe(false); + expect(filter({ ignore: 'vendor-*.js' })(outside)).toBe(true); + }); + + it('rejects files matching `ignore`', () => { + const shouldUpload = filter({ ignore: ['**/vendor-*.js'] }); + expect(shouldUpload(inDist)).toBe(true); + expect(shouldUpload(vendor)).toBe(false); + }); + + it('skips dot files and folders for `assets` unless a pattern names them, like `glob`', () => { + const hidden = path.resolve('.next/static/chunks/main.js'); + expect(filter({ assets: '**/*.js' })(hidden)).toBe(false); + expect(filter({ assets: '.next/**/*.js' })(hidden)).toBe(true); + expect(filter({ assets: '**/*.js' })(inDist)).toBe(true); + }); + + it('matches dot files and folders for `ignore`, like `glob`', () => { + const hidden = path.resolve('.next/static/chunks/main.js'); + expect(filter({ ignore: '**/*.js' })(hidden)).toBe(false); + expect(filter({ assets: '.next/**/*.js', ignore: '**/chunks/*.js' })(hidden)).toBe(false); + }); + + it('applies `ignore` after `assets`', () => { + const shouldUpload = filter({ assets: 'dist/**', ignore: 'dist/app/vendor-*.js' }); + expect(shouldUpload(inDist)).toBe(true); + expect(shouldUpload(vendor)).toBe(false); + }); +}); + +describe('createUploadFilter and the upload from disk', () => { + let root: string; + let files: string[]; + // Shares only the file system root with the working directory of the test. + const farAway = __filename; + const farAwayPattern = escapeGlob(farAway.split(path.sep).join('/')); + + beforeAll(() => { + root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-upload-filter-'))); + files = ['app/dist/main.js', 'app/dist/vendor.js', 'app/.cache/chunk.js', 'lib/dist/main.js', 'lib/dist/util.js'] + .map(file => path.join(root, file)) + .concat(farAway); + for (const file of files.slice(0, -1)) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, ''); + } + vi.spyOn(process, 'cwd').mockReturnValue(path.join(root, 'app')); + }); + + afterAll(() => { + vi.restoreAllMocks(); + fs.rmSync(root, { recursive: true, force: true }); + }); + + it.each<{ assets: string[]; ignore?: string[] }>([ + { assets: ['dist/**'] }, + { assets: ['./dist/*.js'] }, + { assets: ['**/*.js'] }, + { assets: ['**/*'] }, + { assets: ['.cache/**'] }, + { assets: ['{dist,.cache}/*.js'] }, + { assets: ['../lib/**/*.js'] }, + { assets: ['../lib/dist/main.js'] }, + { assets: ['../**/*.js'] }, + { assets: ['../*/dist/main.js'] }, + { assets: ['../../*/lib/dist/*.js'] }, + { assets: ['dist/../../lib/dist/util.js'] }, + { assets: [farAwayPattern] }, + { assets: ['../**/*.js'], ignore: ['../lib/**'] }, + { assets: ['../**/*.js'], ignore: ['../lib/dist/main.js'] }, + { assets: ['../**/*.js'], ignore: ['../**/main.js'] }, + { assets: ['../**/*.js'], ignore: ['**/main.js'] }, + { assets: ['../**/*.js'], ignore: ['./dist/**'] }, + { assets: ['../**/*.js'], ignore: ['dist'] }, + { assets: ['../**/*.js', '.cache/*.js'], ignore: ['**/.cache/**'] }, + { assets: ['../**/*.js', farAwayPattern], ignore: ['**/upload-filter.test.ts'] }, + { assets: ['../**/*.js', farAwayPattern], ignore: ['upload-filter.test.ts'] }, + { assets: ['../**/*.js', farAwayPattern], ignore: [farAwayPattern] }, + ])('accepts the files that `glob` finds for %j', async sourcemaps => { + const onDisk = await globFiles(sourcemaps.assets, { ignore: sourcemaps.ignore }); + + expect(files.filter(filter(sourcemaps)).sort()).toEqual(onDisk.sort()); + }); +}); diff --git a/packages/bundler-plugins/test/core/upload-staging.test.ts b/packages/bundler-plugins/test/core/upload-staging.test.ts index ab87bf4f133f..29437eff7c0e 100644 --- a/packages/bundler-plugins/test/core/upload-staging.test.ts +++ b/packages/bundler-plugins/test/core/upload-staging.test.ts @@ -65,7 +65,7 @@ describe('createUploadStaging', () => { code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', - fileName: 'index.js', + chunkPath: path.join('/out', 'index.js'), }); expect(result).toBe('staged'); @@ -86,7 +86,12 @@ describe('createUploadStaging', () => { const staging = await manager.createUploadStaging(); try { - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out/assets', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out/assets', + chunkPath: path.join('/out/assets', 'a.js'), + }); } finally { await staging.discard(); } @@ -99,11 +104,21 @@ describe('createUploadStaging', () => { const staging = await manager.createUploadStaging(); try { - expect(await staging.stage({ code: 'console.log(1)', mapSource: MAP, mapDir: '/out', fileName: 'a.js' })).toBe( - 'skipped', - ); expect( - await staging.stage({ code: chunk(DEBUG_ID), mapSource: undefined, mapDir: '/out', fileName: 'b.js' }), + await staging.stage({ + code: 'console.log(1)', + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }), + ).toBe('skipped'); + expect( + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: undefined, + mapDir: '/out', + chunkPath: path.join('/out', 'b.js'), + }), ).toBe('skipped'); expect(staging.count).toBe(0); expect(fs.readdirSync(staging.dir)).toEqual([]); @@ -112,11 +127,53 @@ describe('createUploadStaging', () => { } }); + it('does not stage chunks excluded by `sourcemaps.ignore` and leaves them uncounted', async () => { + const manager = createSentryBuildPluginManager( + { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'r', create: false, finalize: false }, + telemetry: false, + sourcemaps: { ignore: ['**/vendor.js'] }, + }, + { buildTool: 'rollup', loggerPrefix: '[test]' }, + ); + const staging = await manager.createUploadStaging(); + + await expect( + staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: tmpDir, + chunkPath: path.join(tmpDir, 'vendor.js'), + }), + ).resolves.toBe('excluded'); + await expect( + staging.stage({ + code: chunk(OTHER_DEBUG_ID), + mapSource: MAP, + mapDir: tmpDir, + chunkPath: path.join(tmpDir, 'main.js'), + }), + ).resolves.toBe('staged'); + + expect(staging.count).toBe(1); + expect(staging.stagedChunkPaths.has(path.join(tmpDir, 'vendor.js'))).toBe(false); + expect(fs.readdirSync(staging.dir).sort()).toEqual([`${OTHER_DEBUG_ID}-0.js`, `${OTHER_DEBUG_ID}-0.js.map`]); + await staging.discard(); + }); + it('removes the folder on discard', async () => { const manager = createManager(); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); await staging.discard(); await staging.discard(); @@ -138,7 +195,12 @@ describe('createUploadStaging', () => { const manager = createManager(); const errorSpy = vi.spyOn(manager.logger, 'error'); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); try { await staging.keep('The upload did not run.'); @@ -170,7 +232,12 @@ describe('createUploadStaging', () => { it('uploads the staging folder and removes it afterwards', async () => { const manager = createManager(); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); const result = await manager.uploadSourcemaps([], { staging }); @@ -187,7 +254,12 @@ describe('createUploadStaging', () => { ); const manager = createManager(); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: tmpDir, fileName: 'index.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: tmpDir, + chunkPath: path.join(tmpDir, 'index.js'), + }); let uploadedFiles: string[] = []; let uploadedMaps = ''; @@ -214,7 +286,12 @@ describe('createUploadStaging', () => { const manager = createManager(); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: outDir, fileName: 'staged.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: outDir, + chunkPath: path.join(outDir, 'staged.js'), + }); let uploadedFiles: string[] = []; mockSourcemapUpload.mockImplementation(async ({ directory }: { directory: string }) => { @@ -235,7 +312,12 @@ describe('createUploadStaging', () => { const manager = createManager(); const warnSpy = vi.spyOn(manager.logger, 'warn'); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); await manager.uploadSourcemaps([], { staging }); @@ -262,7 +344,12 @@ describe('createUploadStaging', () => { { buildTool: 'rollup', loggerPrefix: '[test]' }, ); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); const result = await manager.uploadSourcemaps([], { staging }); @@ -276,7 +363,12 @@ describe('createUploadStaging', () => { const manager = createManager(); const errorSpy = vi.spyOn(manager.logger, 'error'); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); try { const result = await manager.uploadSourcemaps([], { staging }); @@ -292,7 +384,12 @@ describe('createUploadStaging', () => { it('uploads the staging folder next to the direct paths when prepareArtifacts is false', async () => { const manager = createManager(); const staging = await manager.createUploadStaging(); - await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + chunkPath: path.join('/out', 'a.js'), + }); const result = await manager.uploadSourcemaps(['.next/server'], { staging, prepareArtifacts: false }); @@ -378,7 +475,7 @@ describe('sourceMaps', () => { expect(manager.sourceMaps.decide('unset')).toEqual({ enableHidden: false, deleteAfterUpload: false }); const staging = await manager.createUploadStaging(); - expect(await staging.stage({ code: '', mapSource: undefined, mapDir: '', fileName: 'a.js' })).toBe('skipped'); + expect(await staging.stage({ code: '', mapSource: undefined, mapDir: '', chunkPath: 'a.js' })).toBe('skipped'); expect(await manager.uploadSourcemaps([], { staging })).toEqual({ uploaded: false }); }); }); diff --git a/packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts b/packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts new file mode 100644 index 000000000000..1ff087262907 --- /dev/null +++ b/packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as esbuild from 'esbuild'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { sentryEsbuildPlugin } from '../../src/esbuild'; + +describe('entry points without a default export', () => { + let tmpDir: string; + let entry: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-esbuild-entry-exports-')); + entry = path.join(tmpDir, 'entry.js'); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function build(logOverride?: Record): Promise { + return esbuild.build({ + entryPoints: [entry], + bundle: true, + write: false, + format: 'esm', + logLevel: 'silent', + absWorkingDir: tmpDir, + logOverride, + plugins: [sentryEsbuildPlugin({ telemetry: false, sourcemaps: { disable: 'disable-upload' } })], + }); + } + + it.each([ + ['an ES module without exports', 'export {};'], + ['an ES module with only named exports', 'export const answer = 42;'], + ['a plain script', 'console.log("hello");'], + ])('does not warn for %s', async (_name, source) => { + fs.writeFileSync(entry, source); + + const result = await build(); + + expect(result.warnings).toEqual([]); + expect(result.outputFiles?.[0]?.text).toContain('_sentryDebugIds'); + }); + + it('keeps named exports of the entry point', async () => { + fs.writeFileSync(entry, 'export const answer = 42;'); + + const result = await build(); + + expect(result.warnings).toEqual([]); + expect(result.outputFiles?.[0]?.text).toMatch(/export\s*{[^}]*answer[^}]*}/); + }); + + it('keeps a user-provided override for the same esbuild message', async () => { + fs.writeFileSync(entry, 'export {};'); + + await expect(build({ 'import-is-undefined': 'error' })).rejects.toThrow( + /import-is-undefined|will always be undefined/, + ); + }); +}); diff --git a/packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts b/packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts new file mode 100644 index 000000000000..0c28e0b13076 --- /dev/null +++ b/packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts @@ -0,0 +1,513 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { rollup } from 'rollup'; +import { rolldown } from 'rolldown'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { sentryRollupPlugin } from '../../src/rollup'; +import type { Options } from '../../src/core'; + +const { mockSourcemapUpload, mockReleaseCreate } = vi.hoisted(() => ({ + mockSourcemapUpload: vi.fn(), + mockReleaseCreate: vi.fn(), +})); + +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + release: { create: mockReleaseCreate, finalize: vi.fn(), 'set-commits': vi.fn(), deploy: vi.fn() }, + sourcemap: { upload: mockSourcemapUpload, inject: vi.fn() }, + run: vi.fn(), + }), +})); + +const DEBUG_ID_MARKER = /sentry-dbid-([0-9a-f-]{36})/; + +const credentials: Options = { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'test-release', create: false, finalize: false }, + telemetry: false, + silent: true, +}; + +type Upload = { directory: string; files: Record }; + +describe('sourcemaps.deleteSourcemapsAfterUpload (rollup / rolldown)', () => { + let tmpDir: string; + let entry: string; + let outDir: string; + let uploads: Upload[]; + + beforeEach(() => { + vi.clearAllMocks(); + uploads = []; + mockSourcemapUpload.mockImplementation(async ({ directory }: { directory: string }) => { + const files: Record = {}; + for (const file of fs.readdirSync(directory)) { + files[file] = fs.readFileSync(path.join(directory, file), 'utf8'); + } + uploads.push({ directory, files }); + }); + + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-rollup-delete-maps-')); + entry = path.join(tmpDir, 'entry.js'); + outDir = path.join(tmpDir, 'dist'); + fs.writeFileSync(entry, 'export const answer = 42;\nconsole.log(answer);\n'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function trackStagingFolders(): string[] { + const createdFolders: string[] = []; + const mkdtemp = fs.promises.mkdtemp; + vi.spyOn(fs.promises, 'mkdtemp').mockImplementation(async (...args: Parameters) => { + const folder = await mkdtemp(...args); + createdFolders.push(folder); + return folder; + }); + return createdFolders; + } + + function expectUploadedPair(upload: Upload, chunk: string): void { + const debugId = chunk.match(DEBUG_ID_MARKER)?.[1]; + expect(debugId).toBeDefined(); + expect(Object.keys(upload.files).sort()).toEqual([`${debugId}-0.js`, `${debugId}-0.js.map`]); + expect(upload.files[`${debugId}-0.js`]).toContain(`//# debugId=${debugId}`); + expect(JSON.parse(upload.files[`${debugId}-0.js.map`]!)).toMatchObject({ debug_id: debugId }); + expect(fs.existsSync(upload.directory)).toBe(false); + } + + describe.each([ + ['rollup', rollup], + ['rolldown', rolldown], + ])('%s', (_, bundle) => { + it('enables hidden source maps, uploads them and leaves none in the output when the setting is unset', async () => { + const build = await bundle({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + const chunk = fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8'); + expect(chunk).not.toContain('sourceMappingURL'); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, chunk); + }); + + it('stages chunks whose debug ID the bundler injects itself', async () => { + const build = await bundle({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir, sourcemapDebugIds: true }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + const chunk = fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8'); + expect(chunk).toContain('//# debugId='); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, chunk); + }); + + it('switches visible source maps to hidden and removes them when the option is true', async () => { + const build = await bundle({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: true } })], + }); + await build.write({ dir: outDir, sourcemap: true }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + const chunk = fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8'); + expect(chunk).not.toContain('sourceMappingURL'); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, chunk); + }); + }); + + it('keeps hidden source maps the user enabled and uploads them from disk', async () => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + }); + + it('removes hidden source maps the user enabled when the option is true', async () => { + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: true } })], + }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(uploads).toHaveLength(1); + }); + + it('keeps visible source maps when the option is unset', async () => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir, sourcemap: true }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')).toContain('//# sourceMappingURL=entry.js.map'); + }); + + it('enables hidden source maps but keeps them when the option is false', async () => { + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: false } })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')).not.toContain('sourceMappingURL'); + expect(uploads).toHaveLength(1); + expect(Object.keys(uploads[0]!.files)).toHaveLength(2); + }); + + it('does not upload chunks matched by `sourcemaps.ignore` and drops the maps it turned on for them', async () => { + const vendor = path.join(tmpDir, 'vendor.js'); + fs.writeFileSync(vendor, 'console.log("vendor");\n'); + const build = await rollup({ + input: [entry, vendor], + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { ignore: ['**/vendor.js'] } })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'vendor.js']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + expect(Object.keys(uploads[0]!.files)).toHaveLength(2); + }); + + it('keeps the maps the user enabled for chunks matched by `sourcemaps.ignore`', async () => { + const vendor = path.join(tmpDir, 'vendor.js'); + fs.writeFileSync(vendor, 'console.log("vendor");\n'); + const build = await rollup({ + input: [entry, vendor], + plugins: [ + sentryRollupPlugin({ + ...credentials, + sourcemaps: { ignore: ['**/vendor.js'], deleteSourcemapsAfterUpload: true }, + }), + ], + }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'vendor.js', 'vendor.js.map']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + }); + + it('uploads only chunks matched by `sourcemaps.assets`', async () => { + const vendor = path.join(tmpDir, 'vendor.js'); + fs.writeFileSync(vendor, 'console.log("vendor");\n'); + const build = await rollup({ + input: [entry, vendor], + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { assets: [path.join(outDir, 'entry.js')] } })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'vendor.js']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + expect(Object.keys(uploads[0]!.files)).toHaveLength(2); + }); + + it('warns when source maps are turned off', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin({ ...credentials, silent: false })] }); + await build.write({ dir: outDir, sourcemap: false }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source map generation is turned off')); + }); + + it('warns when source maps are inlined and keeps them', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ + input: entry, + plugins: [ + sentryRollupPlugin({ ...credentials, silent: false, sourcemaps: { deleteSourcemapsAfterUpload: true } }), + ], + }); + await build.write({ dir: outDir, sourcemap: 'inline' }); + + expect(fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')).toContain('sourceMappingURL=data:'); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source maps are inlined')); + }); + + it('does not enable source maps when no upload can run', async () => { + const mkdtempSpy = vi.spyOn(fs.promises, 'mkdtemp'); + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, authToken: undefined })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(uploads).toHaveLength(0); + expect(mkdtempSpy).not.toHaveBeenCalled(); + }); + + it('returns no source maps from generate() and does not log or stage', async () => { + const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined); + const mkdtempSpy = vi.spyOn(fs.promises, 'mkdtemp'); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin({ ...credentials, silent: false })] }); + const { output } = await build.generate({ dir: outDir }); + + expect(output.map(file => file.fileName)).toEqual(['entry.js']); + expect(infoSpy).not.toHaveBeenCalledWith(expect.stringContaining('Enabling hidden source maps')); + expect(mkdtempSpy).not.toHaveBeenCalled(); + expect(uploads).toHaveLength(0); + }); + + it.each([ + ['visible output first', false], + ['unset output first', true], + ])('decides for each output when two write into one directory at once (%s)', async (_label, reversed) => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + const visible = { dir: outDir, format: 'es' as const, entryFileNames: '[name].mjs', sourcemap: true }; + const unset = { dir: outDir, format: 'cjs' as const, entryFileNames: '[name].cjs' }; + const outputs = reversed ? [unset, visible] : [visible, unset]; + await Promise.all(outputs.map(output => build.write(output))); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.cjs', 'entry.mjs', 'entry.mjs.map']); + expect(fs.readFileSync(path.join(outDir, 'entry.mjs'), 'utf8')).toContain('//# sourceMappingURL=entry.mjs.map'); + expect(fs.readFileSync(path.join(outDir, 'entry.cjs'), 'utf8')).not.toContain('sourceMappingURL'); + expect(uploads).toHaveLength(2); + // Each upload also globs the directory, so it may carry the other output's files. + const debugId = fs.readFileSync(path.join(outDir, 'entry.cjs'), 'utf8').match(DEBUG_ID_MARKER)?.[1]; + const staged = uploads.find(upload => `${debugId}-0.js` in upload.files); + expect(staged?.files[`${debugId}-0.js`]).toContain(`//# debugId=${debugId}`); + expect(JSON.parse(staged?.files[`${debugId}-0.js.map`] ?? '{}')).toMatchObject({ debug_id: debugId }); + }); + + it('tells a file output apart from a dir output written into the same directory at once', async () => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await Promise.all([ + build.write({ dir: outDir, format: 'es' }), + build.write({ file: path.join(outDir, 'bundle.js'), sourcemap: true }), + ]); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map', 'entry.js']); + expect(uploads).toHaveLength(2); + }); + + it('tells a preserveModules output apart from a bundled one written into the same directory', async () => { + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await Promise.all([ + build.write({ dir: outDir, preserveModules: true, sourcemap: true }), + build.write({ dir: outDir, entryFileNames: 'bundle.js' }), + ]); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'entry.js', 'entry.js.map']); + expect(uploads).toHaveLength(2); + }); + + it('passes the directory of the map file to the rewriteSources hook', async () => { + const mapDirs: Array = []; + const build = await rollup({ + input: entry, + plugins: [ + sentryRollupPlugin({ + ...credentials, + sourcemaps: { + rewriteSources: (source: string, _map: unknown, context?: { mapDir: string }) => { + mapDirs.push(context?.mapDir); + return source; + }, + }, + }), + ], + }); + await build.write({ dir: outDir, sourcemapFileNames: 'maps/[name].js.map' }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(mapDirs).toEqual([path.join(outDir, 'maps')]); + expect(uploads).toHaveLength(1); + }); + + it('uploads each output on its own', async () => { + const otherOutDir = path.join(tmpDir, 'dist-other'); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir }); + await build.write({ dir: otherOutDir, entryFileNames: 'other.js' }); + + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(fs.readdirSync(otherOutDir)).toEqual(['other.js']); + expect(uploads).toHaveLength(2); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + expectUploadedPair(uploads[1]!, fs.readFileSync(path.join(otherOutDir, 'other.js'), 'utf8')); + }); + + it('removes the map asset before other plugins see the written bundle', async () => { + let writtenFiles: string[] = []; + const build = await rollup({ + input: entry, + plugins: [ + sentryRollupPlugin(credentials), + { + name: 'observer', + writeBundle(_options: unknown, bundle: Record) { + writtenFiles = Object.keys(bundle); + }, + }, + ], + }); + await build.write({ dir: outDir }); + + expect(writtenFiles).toEqual(['entry.js']); + }); + + it('keeps the staging folder and logs its path when the release step throws', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const createdFolders = trackStagingFolders(); + mockReleaseCreate.mockRejectedValue(new Error('release failed')); + const build = await rollup({ + input: entry, + plugins: [ + sentryRollupPlugin({ + ...credentials, + silent: false, + release: { name: 'test-release', create: true, finalize: false }, + errorHandler: error => { + throw error; + }, + }), + ], + }); + + await expect(build.write({ dir: outDir })).rejects.toThrow('release failed'); + + try { + expect(fs.readdirSync(outDir)).toEqual(['entry.js']); + expect(uploads).toHaveLength(0); + expect(createdFolders).toHaveLength(1); + expect(fs.existsSync(createdFolders[0]!)).toBe(true); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(`sentry sourcemap upload ${createdFolders[0]}`)); + } finally { + fs.rmSync(createdFolders[0]!, { recursive: true, force: true }); + } + }); + + it('does not upload a staging left behind by a write that never finished', async () => { + const createdFolders = trackStagingFolders(); + const plugins = sentryRollupPlugin(credentials); + let fail = true; + const failingPlugin = { + name: 'failing-post-plugin', + generateBundle: { + order: 'post' as const, + handler: () => { + if (fail) { + throw new Error('write failed'); + } + }, + }, + }; + + const firstBuild = await rollup({ input: entry, plugins: [...plugins, failingPlugin] }); + await expect(firstBuild.write({ dir: outDir })).rejects.toThrow('write failed'); + + fail = false; + fs.writeFileSync(entry, 'export const answer = 43;\nconsole.log(answer);\n'); + const secondBuild = await rollup({ input: entry, plugins: [...plugins, failingPlugin] }); + await secondBuild.write({ dir: outDir }); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + expect(createdFolders).toHaveLength(2); + expect(createdFolders.filter(folder => fs.existsSync(folder))).toEqual([]); + }); + + it('does not pair a new chunk with a stale map left in the output directory', async () => { + fs.mkdirSync(outDir, { recursive: true }); + fs.writeFileSync( + path.join(outDir, 'entry.js.map'), + JSON.stringify({ version: 3, sources: ['stale.ts'], mappings: '' }), + ); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + await build.write({ dir: outDir }); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'entry.js'), 'utf8')); + expect(JSON.stringify(uploads[0]!.files)).not.toContain('stale.ts'); + }); + + it('uploads from disk when writeBundle is called with output options only', async () => { + // SvelteKit defers the main plugin's writeBundle to closeBundle and calls it without a bundle. + const [main, post] = sentryRollupPlugin({ ...credentials, sourcemaps: { deleteSourcemapsAfterUpload: false } }); + const build = await rollup({ input: entry, plugins: [{ ...main, writeBundle: undefined }, post] }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + expect(uploads).toHaveLength(0); + + await main.writeBundle({ dir: outDir }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(uploads).toHaveLength(1); + expect(Object.keys(uploads[0]!.files)).toHaveLength(2); + }); + + it('removes the staging folder when staging a chunk fails', async () => { + const createdFolders = trackStagingFolders(); + const writeFile = fs.promises.writeFile; + vi.spyOn(fs.promises, 'writeFile').mockImplementation(async (...args: Parameters) => { + if (String(args[0]).includes('sentry-bundler-plugin-upload-')) { + throw new Error('disk full'); + } + return writeFile(...args); + }); + const build = await rollup({ input: entry, plugins: [sentryRollupPlugin(credentials)] }); + + await expect(build.write({ dir: outDir })).rejects.toThrow('disk full'); + + expect(createdFolders).toHaveLength(1); + expect(fs.existsSync(createdFolders[0]!)).toBe(false); + expect(uploads).toHaveLength(0); + }); + + const foreignDebugIdPlugin = { + name: 'foreign-debug-id', + renderChunk: (code: string) => `${code}\n//# debugId=11111111-2222-3333-4444-555555555555`, + }; + + it('drops the map it turned on for a chunk it could not prepare and warns', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ + input: entry, + plugins: [foreignDebugIdPlugin, sentryRollupPlugin({ ...credentials, silent: false })], + }); + await build.write({ dir: outDir }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js']); + expect(uploads).toHaveLength(0); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('was removed from the build output')); + }); + + it('keeps the map the user enabled for a chunk it could not prepare and warns', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const build = await rollup({ + input: entry, + plugins: [ + foreignDebugIdPlugin, + sentryRollupPlugin({ ...credentials, silent: false, sourcemaps: { deleteSourcemapsAfterUpload: true } }), + ], + }); + await build.write({ dir: outDir, sourcemap: 'hidden' }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('stays in the build output')); + }); + + it('leaves "disable-upload" builds untouched', async () => { + const build = await rollup({ + input: entry, + plugins: [sentryRollupPlugin({ ...credentials, sourcemaps: { disable: 'disable-upload' } })], + }); + await build.write({ dir: outDir, sourcemap: true }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['entry.js', 'entry.js.map']); + expect(uploads).toHaveLength(0); + }); +}); diff --git a/packages/bundler-plugins/test/rollup/public-api.test.ts b/packages/bundler-plugins/test/rollup/public-api.test.ts index aee8cdb5034b..85fa16562da1 100644 --- a/packages/bundler-plugins/test/rollup/public-api.test.ts +++ b/packages/bundler-plugins/test/rollup/public-api.test.ts @@ -111,7 +111,7 @@ describe('sentryRollupPlugin', () => { vi.clearAllMocks(); }); - it('returns an array of rollup plugins (although only one)', () => { + it('returns the main plugin and the upload staging plugin', () => { const plugins = sentryRollupPlugin({ authToken: 'test-token', org: 'test-org', @@ -119,9 +119,13 @@ describe('sentryRollupPlugin', () => { }) as Plugin[]; expect(Array.isArray(plugins)).toBe(true); - expect(plugins).toHaveLength(1); + expect(plugins.map(plugin => plugin.name)).toEqual(['sentry-rollup-plugin', 'sentry-rollup-upload-staging']); + }); + + it('returns only the noop plugin when disabled', () => { + const plugins = sentryRollupPlugin({ disable: true }) as Plugin[]; - expect(plugins[0]?.name).toBe('sentry-rollup-plugin'); + expect(plugins.map(plugin => plugin.name)).toEqual(['sentry-noop-plugin']); }); }); diff --git a/packages/bundler-plugins/test/rollup/source-map-handling.test.ts b/packages/bundler-plugins/test/rollup/source-map-handling.test.ts new file mode 100644 index 000000000000..8ab640fc3737 --- /dev/null +++ b/packages/bundler-plugins/test/rollup/source-map-handling.test.ts @@ -0,0 +1,99 @@ +import * as path from 'path'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createSentryBuildPluginManager } from '../../src/core/build-plugin-manager'; +import { createSourceMapHandling } from '../../src/rollup/source-map-handling'; +import type { OutputBundle } from '../../src/rollup/debug-id-stamping'; + +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + release: { create: vi.fn(), finalize: vi.fn(), 'set-commits': vi.fn(), deploy: vi.fn() }, + sourcemap: { upload: vi.fn(), inject: vi.fn() }, + run: vi.fn(), + }), +})); + +const DEBUG_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +const OTHER_DEBUG_ID = '11111111-2222-3333-4444-555555555555'; +const THIRD_DEBUG_ID = '99999999-8888-7777-6666-555555555555'; +const MAP = JSON.stringify({ version: 3, sources: ['index.ts'], mappings: 'AAAA' }); + +function chunk(debugId: string): string { + return `console.log(1);\n;!function(){try{var e={},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();`; +} + +function bundleWith(debugId: string): OutputBundle { + return { + 'index.js': { type: 'chunk', fileName: 'index.js', code: chunk(debugId) }, + 'index.js.map': { type: 'asset', fileName: 'index.js.map', source: MAP }, + }; +} + +describe('createSourceMapHandling', () => { + const outputOptions = { dir: 'out' }; + let handling: ReturnType; + + beforeEach(() => { + const manager = createSentryBuildPluginManager( + { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'r', create: false, finalize: false }, + telemetry: false, + silent: true, + }, + { buildTool: 'rollup', loggerPrefix: '[test]' }, + ); + handling = createSourceMapHandling(manager); + handling.setDecision(path.resolve('out'), { + match: {}, + kind: 'unset', + decision: { enableHidden: true, deleteAfterUpload: true }, + }); + }); + + it('hands the oldest staging to a writeBundle call without a bundle', async () => { + const bundle = bundleWith(DEBUG_ID); + await handling.stageSourceMaps(outputOptions, bundle, true); + expect(Object.keys(bundle)).toEqual(['index.js']); + + const staging = await handling.takeStaging(outputOptions, undefined); + + expect(staging?.count).toBe(1); + await staging?.discard(); + expect(await handling.takeStaging(outputOptions, undefined)).toBeUndefined(); + }); + + it('discards a staging whose chunks differ from the written bundle', async () => { + const stale = bundleWith(OTHER_DEBUG_ID); + await handling.stageSourceMaps(outputOptions, stale, true); + const current = bundleWith(DEBUG_ID); + await handling.stageSourceMaps(outputOptions, current, true); + + const staging = await handling.takeStaging(outputOptions, current); + + expect(staging?.count).toBe(1); + await staging?.discard(); + expect(await handling.takeStaging(outputOptions, current)).toBeUndefined(); + }); + + it('keeps the staging of a parallel output while it discards a superseded one', async () => { + const es = { dir: 'out', format: 'es' }; + const cjs = { dir: 'out', format: 'cjs' }; + const decision = { enableHidden: true, deleteAfterUpload: true }; + handling.setDecision(path.resolve('out'), { match: { format: 'es' }, kind: 'unset', decision }); + handling.setDecision(path.resolve('out'), { match: { format: 'cjs' }, kind: 'unset', decision }); + await handling.stageSourceMaps(es, bundleWith(OTHER_DEBUG_ID), true); + + const esBundle = bundleWith(DEBUG_ID); + const cjsBundle = bundleWith(THIRD_DEBUG_ID); + await Promise.all([handling.stageSourceMaps(es, esBundle, true), handling.stageSourceMaps(cjs, cjsBundle, true)]); + + const esStaging = await handling.takeStaging(es, esBundle); + const cjsStaging = await handling.takeStaging(cjs, cjsBundle); + expect(esStaging?.count).toBe(1); + expect(cjsStaging?.count).toBe(1); + await esStaging?.discard(); + await cjsStaging?.discard(); + }); +}); diff --git a/packages/bundler-plugins/test/vite/delete-sourcemaps-after-upload.test.ts b/packages/bundler-plugins/test/vite/delete-sourcemaps-after-upload.test.ts new file mode 100644 index 000000000000..8ab267e0d785 --- /dev/null +++ b/packages/bundler-plugins/test/vite/delete-sourcemaps-after-upload.test.ts @@ -0,0 +1,348 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { build, createBuilder } from 'vite'; +import type { Plugin, ResolvedConfig } from 'vite'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { sentryVitePlugin } from '../../src/vite'; +import type { Options } from '../../src/core'; + +const { mockSourcemapUpload } = vi.hoisted(() => ({ mockSourcemapUpload: vi.fn() })); + +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + release: { create: vi.fn(), finalize: vi.fn(), 'set-commits': vi.fn(), deploy: vi.fn() }, + sourcemap: { upload: mockSourcemapUpload, inject: vi.fn() }, + run: vi.fn(), + }), +})); + +const credentials: Options = { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'test-release', create: false, finalize: false }, + telemetry: false, + silent: true, +}; + +type Upload = { directory: string; files: Record }; + +function listFiles(dir: string): string[] { + return fs + .readdirSync(dir, { recursive: true, encoding: 'utf8' }) + .filter(file => fs.statSync(path.join(dir, file)).isFile()); +} + +function readChunks(dir: string): string[] { + return listFiles(dir) + .filter(file => file.endsWith('.js')) + .map(file => fs.readFileSync(path.join(dir, file), 'utf8')); +} + +describe('sourcemaps.deleteSourcemapsAfterUpload (vite)', () => { + let root: string; + let entry: string; + let outDir: string; + let uploads: Upload[]; + + beforeEach(() => { + vi.clearAllMocks(); + uploads = []; + mockSourcemapUpload.mockImplementation(async ({ directory }: { directory: string }) => { + const files: Record = {}; + for (const file of fs.readdirSync(directory)) { + files[file] = fs.readFileSync(path.join(directory, file), 'utf8'); + } + uploads.push({ directory, files }); + }); + + root = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-vite-delete-maps-')); + entry = path.join(root, 'main.js'); + outDir = path.join(root, 'dist'); + // A dynamic import makes Vite's import analysis regenerate the source map in `generateBundle`. + fs.writeFileSync(entry, "import('./lazy.js').then(m => console.log(m.answer));\n"); + fs.writeFileSync(path.join(root, 'lazy.js'), 'export const answer = 42;\n'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(root, { recursive: true, force: true }); + }); + + function viteConfig(pluginOptions: Options, plugins: Plugin[] = []) { + return { + configFile: false as const, + logLevel: 'silent' as const, + root, + plugins: [...sentryVitePlugin({ ...credentials, ...pluginOptions }), ...plugins], + build: { outDir, minify: false as const, rollupOptions: { input: entry } }, + }; + } + + function expectUploadedPairs(upload: Upload, chunks: string[]): void { + const uploadedNames = Object.keys(upload.files); + expect(uploadedNames.filter(name => name.endsWith('.js'))).toHaveLength(chunks.length); + + for (const chunk of chunks) { + const debugId = chunk.match(/sentry-dbid-([0-9a-f-]{36})/)?.[1]; + expect(debugId).toBeDefined(); + const mapName = uploadedNames.find(name => name.startsWith(`${debugId}-`) && name.endsWith('.map')); + expect(mapName).toBeDefined(); + expect(JSON.parse(upload.files[mapName!]!)).toMatchObject({ debug_id: debugId }); + } + + expect(fs.existsSync(upload.directory)).toBe(false); + } + + it('enables hidden source maps, uploads them and leaves none in the output when the setting is unset', async () => { + let resolvedSourcemap: unknown; + const spy: Plugin = { + name: 'spy', + configResolved(config: ResolvedConfig) { + resolvedSourcemap = config.build.sourcemap; + }, + }; + + await build(viteConfig({}, [spy])); + + expect(resolvedSourcemap).toBe('hidden'); + const files = listFiles(outDir); + expect(files.filter(file => file.endsWith('.map'))).toEqual([]); + const chunks = readChunks(outDir); + expect(chunks).toHaveLength(2); + for (const chunk of chunks) { + expect(chunk).not.toContain('sourceMappingURL'); + } + + expect(uploads).toHaveLength(1); + expectUploadedPairs(uploads[0]!, chunks); + }); + + it('switches visible source maps to hidden and removes them when the option is true', async () => { + const config = viteConfig({ sourcemaps: { deleteSourcemapsAfterUpload: true } }); + await build({ ...config, build: { ...config.build, sourcemap: true } }); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + const chunks = readChunks(outDir); + for (const chunk of chunks) { + expect(chunk).not.toContain('sourceMappingURL'); + } + expect(uploads).toHaveLength(1); + expectUploadedPairs(uploads[0]!, chunks); + }); + + it('stages chunks whose debug ID the bundler injects itself', async () => { + const config = viteConfig({}); + await build({ + ...config, + build: { ...config.build, rollupOptions: { input: entry, output: { sourcemapDebugIds: true } } }, + }); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + const chunks = readChunks(outDir); + for (const chunk of chunks) { + expect(chunk).toContain('//# debugId='); + } + expect(uploads).toHaveLength(1); + expectUploadedPairs(uploads[0]!, chunks); + }); + + it('removes source maps from an output directory set through rollupOptions', async () => { + const rollupOutDir = path.join(root, 'out', 'rollup'); + const config = viteConfig({}); + await build({ + ...config, + build: { ...config.build, outDir: 'dist', rollupOptions: { input: entry, output: { dir: rollupOutDir } } }, + }); + + expect(listFiles(rollupOutDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(uploads).toHaveLength(1); + expectUploadedPairs(uploads[0]!, readChunks(rollupOutDir)); + }); + + it('keeps source maps an output enables through rollupOptions and uploads them from disk', async () => { + const config = viteConfig({}); + await build({ + ...config, + build: { ...config.build, rollupOptions: { input: entry, output: { sourcemap: true } } }, + }); + + const chunks = readChunks(outDir); + expect(chunks).toHaveLength(2); + expect(chunks.every(chunk => chunk.includes('//# sourceMappingURL='))).toBe(true); + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toHaveLength(2); + expect(uploads).toHaveLength(1); + expect(Object.keys(uploads[0]!.files)).toHaveLength(4); + }); + + it('keeps hidden source maps an output enables through rollupOptions', async () => { + const config = viteConfig({}); + await build({ + ...config, + build: { ...config.build, rollupOptions: { input: entry, output: { sourcemap: 'hidden' } } }, + }); + + const chunks = readChunks(outDir); + expect(chunks).toHaveLength(2); + expect(chunks.some(chunk => chunk.includes('//# sourceMappingURL='))).toBe(false); + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toHaveLength(2); + expect(uploads).toHaveLength(1); + expect(Object.keys(uploads[0]!.files)).toHaveLength(4); + }); + + it('removes hidden source maps an output enables when the option is true', async () => { + const config = viteConfig({ sourcemaps: { deleteSourcemapsAfterUpload: true } }); + await build({ + ...config, + build: { ...config.build, rollupOptions: { input: entry, output: { sourcemap: 'hidden' } } }, + }); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(uploads).toHaveLength(1); + expectUploadedPairs(uploads[0]!, readChunks(outDir)); + }); + + it('keeps hidden source maps the user enabled and uploads them from disk', async () => { + const config = viteConfig({}); + await build({ ...config, build: { ...config.build, sourcemap: 'hidden' } }); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toHaveLength(2); + expect(uploads).toHaveLength(1); + expect(Object.keys(uploads[0]!.files)).toHaveLength(4); + }); + + it('does not upload chunks matched by `sourcemaps.ignore` and drops the maps it turned on for them', async () => { + const vendor = path.join(root, 'vendor.js'); + fs.writeFileSync(vendor, 'console.log("vendor");\n'); + const config = viteConfig({ sourcemaps: { ignore: ['**/vendor-*.js'] } }); + await build({ ...config, build: { ...config.build, rollupOptions: { input: [entry, vendor] } } }); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(uploads).toHaveLength(1); + expectUploadedPairs( + uploads[0]!, + readChunks(outDir).filter(chunk => !chunk.includes('"vendor"')), + ); + }); + + it('decides for each output when two write into one directory', async () => { + const config = viteConfig({}); + await build({ + ...config, + build: { + ...config.build, + rollupOptions: { + input: entry, + output: [ + { format: 'es', entryFileNames: '[name].mjs', chunkFileNames: '[name].mjs', sourcemap: true }, + { format: 'cjs', entryFileNames: '[name].cjs', chunkFileNames: '[name].cjs' }, + ], + }, + }, + }); + + expect(listFiles(outDir).sort()).toEqual([ + 'lazy.cjs', + 'lazy.mjs', + 'lazy.mjs.map', + 'main.cjs', + 'main.mjs', + 'main.mjs.map', + ]); + expect(uploads).toHaveLength(2); + }); + + it('forgets environment decisions of an earlier build of the same plugin instances', async () => { + const config = viteConfig({}); + await build({ ...config, environments: { client: { build: { sourcemap: false } } } }); + expect(listFiles(outDir).some(file => file.endsWith('.map'))).toBe(false); + expect(uploads).toHaveLength(0); + + await build(config); + + expect(listFiles(outDir).some(file => file.endsWith('.map'))).toBe(false); + expect(uploads).toHaveLength(1); + expectUploadedPairs(uploads[0]!, readChunks(outDir)); + }); + + it('does not create a staging folder or log when the build does not write', async () => { + const mkdtempSpy = vi.spyOn(fs.promises, 'mkdtemp'); + const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined); + const config = viteConfig({ silent: false }); + await build({ ...config, build: { ...config.build, write: false } }); + + expect(mkdtempSpy).not.toHaveBeenCalled(); + expect(uploads).toHaveLength(0); + expect(infoSpy).not.toHaveBeenCalledWith(expect.stringContaining('Enabling hidden source maps')); + }); + + it('does not warn about an environment that is not built', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + await build({ ...viteConfig({ silent: false }), environments: { ssr: { build: { sourcemap: false } } } }); + + expect(uploads).toHaveLength(1); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('turned off')); + }); + + it('does not remove source maps when a later plugin turns them off', async () => { + const later: Plugin = { name: 'later', config: () => ({ build: { sourcemap: false } }) }; + + await build(viteConfig({}, [later])); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(uploads).toHaveLength(0); + }); + + it('uploads a client build and an SSR build of the same plugin instances on their own', async () => { + const plugins = sentryVitePlugin(credentials); + const serverOutDir = path.join(root, 'dist-server'); + + await build({ ...viteConfig({}), plugins }); + await build({ + ...viteConfig({}), + plugins, + build: { outDir: serverOutDir, minify: false, ssr: entry }, + }); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(listFiles(serverOutDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(uploads).toHaveLength(2); + }); + + it('keeps source maps in an environment that sets them itself', async () => { + const serverOutDir = path.join(root, 'dist-server'); + const builder = await createBuilder({ + ...viteConfig({}), + environments: { + ssr: { + build: { outDir: serverOutDir, minify: false, sourcemap: 'hidden', rollupOptions: { input: entry } }, + }, + }, + builder: {}, + }); + await builder.buildApp(); + + expect(listFiles(outDir).filter(file => file.endsWith('.map'))).toEqual([]); + expect(listFiles(serverOutDir).filter(file => file.endsWith('.map'))).toHaveLength(2); + expect(uploads).toHaveLength(2); + }); + + it('still restores debug IDs for "disable-upload" builds', async () => { + const config = viteConfig({ sourcemaps: { disable: 'disable-upload' } }); + await build({ ...config, build: { ...config.build, sourcemap: true } }); + + const chunks = readChunks(outDir); + const maps = listFiles(outDir).filter(file => file.endsWith('.map')); + expect(maps).toHaveLength(2); + for (const chunk of chunks) { + const debugId = chunk.match(/sentry-dbid-([0-9a-f-]{36})/)?.[1]; + expect(debugId).toBeDefined(); + const map = maps + .map(file => JSON.parse(fs.readFileSync(path.join(outDir, file), 'utf8'))) + .find(m => m.debug_id === debugId); + expect(map).toBeDefined(); + } + expect(uploads).toHaveLength(0); + }); +}); diff --git a/packages/bundler-plugins/test/vite/public-api.test.ts b/packages/bundler-plugins/test/vite/public-api.test.ts index cade4da1c67a..08fcae0c6212 100644 --- a/packages/bundler-plugins/test/vite/public-api.test.ts +++ b/packages/bundler-plugins/test/vite/public-api.test.ts @@ -24,7 +24,7 @@ describe('sentryVitePlugin', () => { const pluginNames = plugins.map(plugin => plugin.name); - expect(pluginNames).toEqual(expect.arrayContaining(['sentry-vite-plugin'])); + expect(pluginNames).toEqual(['sentry-vite-plugin', 'sentry-vite-upload-staging']); }); it('returns an array of Vite pluginswhen unplugin returns a single plugin', () => { diff --git a/packages/bundler-plugins/test/webpack/delete-sourcemaps-after-upload.test.ts b/packages/bundler-plugins/test/webpack/delete-sourcemaps-after-upload.test.ts new file mode 100644 index 000000000000..789f6d97d9d4 --- /dev/null +++ b/packages/bundler-plugins/test/webpack/delete-sourcemaps-after-upload.test.ts @@ -0,0 +1,463 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { EntryPlugin, SourceMapDevToolPlugin, webpack } from 'webpack'; +import type { Compiler, Configuration, Stats } from 'webpack'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { sentryWebpackPlugin } from '../../src/webpack/index'; +import type { Options } from '../../src/core'; + +const { mockSourcemapUpload, mockReleaseCreate } = vi.hoisted(() => ({ + mockSourcemapUpload: vi.fn(), + mockReleaseCreate: vi.fn(), +})); + +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + release: { create: mockReleaseCreate, finalize: vi.fn(), 'set-commits': vi.fn(), deploy: vi.fn() }, + sourcemap: { upload: mockSourcemapUpload, inject: vi.fn() }, + run: vi.fn(), + }), +})); + +const DEBUG_ID_MARKER = /sentry-dbid-([0-9a-f-]{36})/; + +const credentials: Options = { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'test-release', create: false, finalize: false }, + telemetry: false, + silent: true, +}; + +type Upload = { directory: string; files: Record }; + +function build(config: Configuration | Configuration[]): Promise { + return new Promise((resolve, reject) => { + webpack(config as Configuration, (err, stats) => { + if (err) { + return reject(err); + } + if (!stats || stats.hasErrors()) { + return reject(new Error(stats?.toString() ?? 'no stats')); + } + resolve(stats as Stats); + }); + }); +} + +function run(compiler: Compiler): Promise { + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err || !stats || stats.hasErrors()) { + return reject(err ?? new Error(stats?.toString() ?? 'no stats')); + } + resolve(stats); + }); + }); +} + +function createChildCompilerPlugin(context: string): { apply(compiler: Compiler): void } { + return { + apply(compiler) { + compiler.hooks.make.tapAsync('test-child-compiler', (compilation, callback) => { + const childCompiler = compilation.createChildCompiler('test-child-compiler', { filename: 'worker.js' }, [ + new EntryPlugin(context, './worker.js', { name: 'worker' }), + ]); + childCompiler.runAsChild(error => callback(error ?? undefined)); + }); + }, + }; +} + +describe('sourcemaps.deleteSourcemapsAfterUpload (webpack)', () => { + let tmpDir: string; + let outDir: string; + let uploads: Upload[]; + + beforeEach(() => { + vi.clearAllMocks(); + uploads = []; + mockSourcemapUpload.mockImplementation(async ({ directory }: { directory: string }) => { + const files: Record = {}; + for (const file of fs.readdirSync(directory)) { + files[file] = fs.readFileSync(path.join(directory, file), 'utf8'); + } + uploads.push({ directory, files }); + }); + + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-webpack-delete-maps-')); + outDir = path.join(tmpDir, 'dist'); + fs.writeFileSync(path.join(tmpDir, 'entry.js'), 'export const answer = 42;\nconsole.log(answer);\n'); + fs.writeFileSync(path.join(tmpDir, 'worker.js'), 'globalThis.workerLoaded = true;\n'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function config(pluginOptions: Options = {}, overrides: Configuration = {}): Configuration { + return { + mode: 'production', + context: tmpDir, + entry: './entry.js', + output: { path: outDir, filename: 'bundle.js' }, + plugins: [sentryWebpackPlugin({ ...credentials, ...pluginOptions })], + ...overrides, + }; + } + + // Other test files create staging folders at the same time, so only the + // folders created through this spy are checked. + function trackStagingFolders(): string[] { + const createdFolders: string[] = []; + const mkdtemp = fs.promises.mkdtemp; + vi.spyOn(fs.promises, 'mkdtemp').mockImplementation(async (...args: Parameters) => { + const folder = await mkdtemp(...args); + createdFolders.push(folder); + return folder; + }); + return createdFolders; + } + + function expectUploadedPair( + upload: Upload, + bundle: string, + ): { debugId: string; stagedBundle: string; map: Record } { + const debugId = bundle.match(DEBUG_ID_MARKER)?.[1]; + expect(debugId).toBeDefined(); + const jsName = Object.keys(upload.files).find(name => name.startsWith(`${debugId}-`) && name.endsWith('.js')); + expect(jsName).toBeDefined(); + const stagedBundle = upload.files[jsName!]!; + expect(stagedBundle).toContain(`//# debugId=${debugId}`); + const map = JSON.parse(upload.files[`${jsName}.map`]!); + expect(map).toMatchObject({ debug_id: debugId }); + expect(fs.existsSync(upload.directory)).toBe(false); + return { debugId: debugId!, stagedBundle, map }; + } + + it('enables hidden source maps, uploads them and leaves none in the output when devtool is unset', async () => { + const stats = await build(config()); + + expect(stats.compilation.options.devtool).toBe('hidden-source-map'); + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + const bundle = fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8'); + expect(bundle).not.toContain('sourceMappingURL'); + + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, bundle); + }); + + it('switches visible source maps to hidden and removes them when the option is true', async () => { + const stats = await build( + config({ sourcemaps: { deleteSourcemapsAfterUpload: true } }, { devtool: 'cheap-module-source-map' }), + ); + + expect(stats.compilation.options.devtool).toBe('hidden-cheap-module-source-map'); + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8')).not.toContain('sourceMappingURL'); + expect(uploads).toHaveLength(1); + }); + + it('stages bundles whose debug ID webpack injects itself', async () => { + const stats = await build( + config({ sourcemaps: { deleteSourcemapsAfterUpload: true } }, { devtool: 'source-map-debugids' }), + ); + + expect(stats.compilation.options.devtool).toBe('hidden-source-map-debugids'); + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + const bundle = fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8'); + expect(bundle).toContain('//# debugId='); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, bundle); + }); + + it('keeps hidden source maps the user enabled and uploads them from disk', async () => { + await build(config({}, { devtool: 'hidden-source-map' })); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8')); + }); + + it('keeps visible source maps when the option is unset', async () => { + const stats = await build(config({}, { devtool: 'source-map' })); + + expect(stats.compilation.options.devtool).toBe('source-map'); + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map']); + }); + + it('enables hidden source maps but keeps them when the option is false', async () => { + await build(config({ sourcemaps: { deleteSourcemapsAfterUpload: false } })); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map']); + expect(fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8')).not.toContain('sourceMappingURL'); + expect(uploads).toHaveLength(1); + }); + + it('warns when source maps are turned off', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + await build(config({ silent: false }, { devtool: false })); + + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source map generation is turned off')); + }); + + it('warns when source maps are inlined and keeps them', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + await build( + config({ silent: false, sourcemaps: { deleteSourcemapsAfterUpload: true } }, { devtool: 'inline-source-map' }), + ); + + expect(fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8')).toContain('sourceMappingURL=data:'); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source maps are inlined')); + }); + + it('leaves a manual SourceMapDevToolPlugin alone', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const stats = await build({ + ...config({ silent: false }), + devtool: false, + plugins: [ + new SourceMapDevToolPlugin({ filename: '[file].map' }), + sentryWebpackPlugin({ ...credentials, silent: false }), + ], + }); + + expect(stats.compilation.options.devtool).toBe(false); + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map']); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Source map generation is turned off')); + }); + + it('does not touch development builds', async () => { + const stats = await build(config({}, { mode: 'development' })); + + expect(stats.compilation.options.devtool).toBe('eval'); + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + }); + + it('does not enable source maps when no upload can run', async () => { + const mkdtempSpy = vi.spyOn(fs.promises, 'mkdtemp'); + const stats = await build(config({ authToken: undefined })); + + expect(stats.compilation.options.devtool).toBe(false); + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(uploads).toHaveLength(0); + expect(mkdtempSpy).not.toHaveBeenCalled(); + }); + + it('stages the final content of content-hashed bundles', async () => { + const output = { path: outDir, filename: '[name].[contenthash].js' }; + await build(config({}, { output })); + + const bundleFileName = fs.readdirSync(outDir).find(file => file.endsWith('.js'))!; + const bundle = fs.readFileSync(path.join(outDir, bundleFileName), 'utf8'); + expect(fs.readdirSync(outDir)).toEqual([bundleFileName]); + + const { debugId, stagedBundle, map } = expectUploadedPair(uploads[0]!, bundle); + expect(stagedBundle.replace(`\n//# debugId=${debugId}`, '')).toBe(bundle); + + // The map webpack writes itself is the reference for the staged map. + // Upload preparation rewrites `sources`, so compare the other fields. + const referenceDir = path.join(tmpDir, 'dist-reference'); + await build( + config({ sourcemaps: { deleteSourcemapsAfterUpload: false } }, { output: { ...output, path: referenceDir } }), + ); + const referenceMapName = fs.readdirSync(referenceDir).find(file => file.endsWith('.map'))!; + const referenceMap = JSON.parse(fs.readFileSync(path.join(referenceDir, referenceMapName), 'utf8')); + expect(map).toMatchObject({ + file: referenceMap.file, + mappings: referenceMap.mappings, + names: referenceMap.names, + sourcesContent: referenceMap.sourcesContent, + }); + }); + + it('does not upload chunks matched by `sourcemaps.ignore` and drops the maps it turned on for them', async () => { + fs.writeFileSync(path.join(tmpDir, 'vendor.js'), 'console.log("vendor");\n'); + await build( + config( + { sourcemaps: { ignore: ['**/vendor.js'] } }, + { entry: { bundle: './entry.js', vendor: './vendor.js' }, output: { path: outDir, filename: '[name].js' } }, + ), + ); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'vendor.js']); + expect(uploads).toHaveLength(1); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8')); + expect(Object.keys(uploads[0]!.files)).toHaveLength(2); + }); + + it('serves every compiler of a MultiCompiler on its own', async () => { + const otherOutDir = path.join(tmpDir, 'dist-other'); + const plugin = sentryWebpackPlugin(credentials); + + await build([ + { ...config(), plugins: [plugin] }, + { ...config(), output: { path: otherOutDir, filename: 'other.js' }, plugins: [plugin] }, + ]); + + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(fs.readdirSync(otherOutDir)).toEqual(['other.js']); + expect(uploads).toHaveLength(2); + }); + + it('stages the bundles of child compilers with the parent compilation', async () => { + await build({ ...config(), plugins: [sentryWebpackPlugin(credentials), createChildCompilerPlugin(tmpDir)] }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'worker.js']); + expect(uploads).toHaveLength(1); + expect(Object.keys(uploads[0]!.files).filter(name => name.endsWith('.js'))).toHaveLength(2); + expectUploadedPair(uploads[0]!, fs.readFileSync(path.join(outDir, 'bundle.js'), 'utf8')); + }); + + it('leaves no staging folder behind over several compilations', async () => { + const createdFolders = trackStagingFolders(); + + const compiler = webpack(config()); + await run(compiler); + await run(compiler); + await new Promise((resolve, reject) => compiler.close(err => (err ? reject(err) : resolve()))); + + expect(uploads).toHaveLength(2); + expect(createdFolders).toHaveLength(2); + expect(createdFolders.filter(folder => fs.existsSync(folder))).toEqual([]); + }); + + it('keeps the staging folder when an earlier afterEmit tap fails before the upload', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const createdFolders = trackStagingFolders(); + const failingPlugin = { + apply(compiler: Compiler) { + compiler.hooks.afterEmit.tapAsync('failing', (_compilation, callback) => + callback(new Error('afterEmit failed')), + ); + }, + }; + + await expect( + build({ ...config(), plugins: [failingPlugin, sentryWebpackPlugin({ ...credentials, silent: false })] }), + ).rejects.toThrow('afterEmit failed'); + + try { + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(uploads).toHaveLength(0); + expect(createdFolders).toHaveLength(1); + expect(fs.existsSync(createdFolders[0]!)).toBe(true); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(`sentry sourcemap upload ${createdFolders[0]}`)); + } finally { + fs.rmSync(createdFolders[0]!, { recursive: true, force: true }); + } + }); + + it('keeps the staging folder and logs its path when the release step throws', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + const createdFolders = trackStagingFolders(); + mockReleaseCreate.mockRejectedValue(new Error('release failed')); + + await expect( + build( + config({ + silent: false, + release: { name: 'test-release', create: true, finalize: false }, + errorHandler: error => { + throw error; + }, + }), + ), + ).rejects.toThrow('release failed'); + + try { + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(uploads).toHaveLength(0); + expect(createdFolders).toHaveLength(1); + expect(fs.existsSync(createdFolders[0]!)).toBe(true); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(`sentry sourcemap upload ${createdFolders[0]}`)); + } finally { + fs.rmSync(createdFolders[0]!, { recursive: true, force: true }); + } + }); + + const breakSourceMap = { + apply(compiler: Compiler) { + compiler.hooks.thisCompilation.tap('break-source-map', compilation => { + compilation.hooks.processAssets.tap( + { name: 'break-source-map', stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER }, + () => compilation.updateAsset('bundle.js.map', new compiler.webpack.sources.RawSource('not json')), + ); + }); + }, + }; + + it('drops the map it turned on for a bundle it could not prepare and warns', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + await build({ + ...config({ silent: false }), + plugins: [sentryWebpackPlugin({ ...credentials, silent: false }), breakSourceMap], + }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js']); + expect(uploads).toHaveLength(0); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('was removed from the build output')); + }); + + it('keeps the map the user enabled for a bundle it could not prepare and warns', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + await build({ + ...config({ silent: false }, { devtool: 'hidden-source-map' }), + plugins: [ + sentryWebpackPlugin({ ...credentials, silent: false, sourcemaps: { deleteSourcemapsAfterUpload: true } }), + breakSourceMap, + ], + }); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map']); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('stays in the build output')); + }); + + it('drops the CSS source maps it turned on', async () => { + fs.writeFileSync(path.join(tmpDir, 'style.css'), 'body { color: red; }\n'); + fs.writeFileSync(path.join(tmpDir, 'entry.js'), "import './style.css';\nconsole.log(42);\n"); + await build(config({}, { experiments: { css: true } })); + + const files = fs.readdirSync(outDir); + expect(files.filter(file => file.endsWith('.css'))).toHaveLength(1); + expect(files.filter(file => file.endsWith('.map'))).toEqual([]); + expect(uploads).toHaveLength(1); + }); + + it('keeps the CSS source maps the user enabled', async () => { + fs.writeFileSync(path.join(tmpDir, 'style.css'), 'body { color: red; }\n'); + fs.writeFileSync(path.join(tmpDir, 'entry.js'), "import './style.css';\nconsole.log(42);\n"); + await build( + config( + { sourcemaps: { deleteSourcemapsAfterUpload: true } }, + { devtool: 'hidden-source-map', experiments: { css: true } }, + ), + ); + + const files = fs.readdirSync(outDir); + expect(files.filter(file => file.endsWith('.css.map'))).toHaveLength(1); + expect(files.filter(file => file.endsWith('.js.map'))).toEqual([]); + expect(uploads).toHaveLength(1); + }); + + it('treats eval without source maps as turned off', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + await build(config({ silent: false }, { devtool: 'eval' })); + + expect(fs.readdirSync(outDir)).toEqual(['bundle.js']); + expect(uploads).toHaveLength(0); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Source map generation is turned off')); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('Source maps are inlined')); + }); + + it('leaves "disable-upload" builds untouched', async () => { + await build(config({ sourcemaps: { disable: 'disable-upload' } }, { devtool: 'source-map' })); + + expect(fs.readdirSync(outDir).sort()).toEqual(['bundle.js', 'bundle.js.map']); + expect(uploads).toHaveLength(0); + }); +}); diff --git a/packages/bundler-plugins/test/webpack/release-injection.test.ts b/packages/bundler-plugins/test/webpack/release-injection.test.ts index 55ee52f5a944..0b7eb75b7a52 100644 --- a/packages/bundler-plugins/test/webpack/release-injection.test.ts +++ b/packages/bundler-plugins/test/webpack/release-injection.test.ts @@ -43,7 +43,7 @@ describe('release injection', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it('injects the release only into initial chunks', async () => { + it('injects the release only into initial chunks', { timeout: 15_000 }, async () => { const outDir = path.join(tmpDir, 'dist'); await build({ diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 3abd81222111..45c71352910f 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/opentelemetry": "11.11.0", "@sentry/server-utils": "11.11.0", @@ -80,7 +80,7 @@ "@sentry-internal/rollup-utils": "11.11.0", "@types/node": "^20.19.0", "acorn": "^8.14.0", - "vite": "^8.3.5", + "vite": "^8.3.6", "wrangler": "4.156.0" }, "scripts": { @@ -91,9 +91,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-cloudflare-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-cloudflare-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts index ca9e75dbef64..8f3c1acea394 100644 --- a/packages/cloudflare/src/client.ts +++ b/packages/cloudflare/src/client.ts @@ -410,6 +410,37 @@ interface BaseCloudflareOptions { */ rpcTracePropagationBindings?: TracePropagationTargets; + /** + * The Queue producer bindings on `env` that outgoing messages propagate trace context to. + * + * Cloudflare Queues has no message headers, so the SDK adds the producer's `sentry-trace` to + * plain-object message bodies under `__sentry_queue_trace__`. Arrays, binary data and other + * non-plain values are sent unchanged. A Sentry-instrumented consumer removes that field before + * your queue handler runs and links its `queue.process` span to every producer in the batch. + * Anywhere else the field arrives as part of the message body, so list only the queues whose + * consumer you know runs Sentry. + * + * The field adds about 80 bytes to each message. Cloudflare rejects messages over 128 KB, so a + * message close to that limit can fail to send once propagation is enabled. + * + * Strings match a binding name exactly, regular expressions match by pattern. Both match + * case-insensitively. + * + * @default [] + * @example + * ```ts + * // Propagate to `env.BILLING_QUEUE` and every `env.JOBS_*` binding + * export default Sentry.withSentry( + * env => ({ + * dsn: env.SENTRY_DSN, + * queueTracePropagationBindings: ['BILLING_QUEUE', /^JOBS_/], + * }), + * handler, + * ); + * ``` + */ + queueTracePropagationBindings?: TracePropagationTargets; + /** * Table names that should stay instrumented even though they match a reserved prefix used by * Durable Object frameworks for their internal SQLite tables: `cf_` (`agents`, `partyserver`, ...) diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 541b507a7d56..92180b841900 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -9,6 +9,8 @@ import { getInstrumented, markAsInstrumented } from './instrument'; import { instrumentDurableObjectHandlers } from './instrumentations/instrumentDurableObjectHandlers'; import { instrumentEnv } from './instrumentations/worker/instrumentEnv'; import { getFinalOptions } from './options'; +import { flushDeferredChannelEvents } from './orchestrion-deferred-channels'; +import { init } from './sdk'; import { instrumentContext } from './utils/instrumentContext'; import { withConstructionIsolationScope } from './utils/invocationScope'; import { hasRpcMeta } from './utils/rpcMeta'; @@ -63,14 +65,24 @@ export function constructInstrumentedDurableObject const prototype = (newTarget as unknown as { prototype?: object }).prototype ?? target.prototype; const cachedFrameworkManagedMethods = frameworkManagedMethodsCache.get(prototype); - const methodsBeforeConstruction = cachedFrameworkManagedMethods ? undefined : resolvePrototypeMethods(prototype); + const methodsBeforeConstruction = cachedFrameworkManagedMethods + ? undefined + : resolveMethodsBeforeFirstConstruction(prototype); // Pass `newTarget` so that subclasses of the instrumented class (e.g. the wrapper classes // created by wrangler's local dev tooling or `@cloudflare/vitest-pool-workers`) keep their // own prototype — otherwise subclass methods disappear and `instanceof` checks break. - const obj = withConstructionIsolationScope( - () => Reflect.construct(target, [context, instrumentedEnv], newTarget) as T, - ); + const obj = withConstructionIsolationScope(() => { + // The constructor can already use instrumented libraries, e.g. open a pi-durable Harness in + // `blockConcurrencyWhile`. Their channel integrations only subscribe in `init()`, and an isolate + // that runs only this Durable Object has no earlier handler that called it. A per-invocation + // client (`cacheClient: false`) would have no invocation to flush it, so it waits for one. + if (options.cacheClient !== false) { + init({ ...options }); + flushDeferredChannelEvents(); + } + return Reflect.construct(target, [context, instrumentedEnv], newTarget) as T; + }); const frameworkManagedMethods = resolveFrameworkManagedMethods( prototype, @@ -85,6 +97,26 @@ export function constructInstrumentedDurableObject } const frameworkManagedMethodsCache = new WeakMap>(); +const methodsBeforeFirstConstructionCache = new WeakMap>(); + +/** + * Collects the prototype's methods as they were before any instance was constructed. + * + * A constructor can throw after a framework has already installed its wrappers (the `agents` SDK + * wraps `@callable()` methods before storage initialization, which can fail). The next construction + * would then see those wrappers as the originals and treat the methods as unmanaged, so the methods + * collected on the first attempt are kept until a construction succeeds. + */ +function resolveMethodsBeforeFirstConstruction(prototype: object): Map { + let methods = methodsBeforeFirstConstructionCache.get(prototype); + + if (!methods) { + methods = resolvePrototypeMethods(prototype); + methodsBeforeFirstConstructionCache.set(prototype, methods); + } + + return methods; +} /** * Collects the methods visible from a prototype, using normal property lookup precedence. @@ -140,6 +172,7 @@ function resolveFrameworkManagedMethods( } frameworkManagedMethodsCache.set(prototype, managed); + methodsBeforeFirstConstructionCache.delete(prototype); return managed; } diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts index 18cf562d95be..a3de0f67b24b 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentEnv.ts @@ -12,7 +12,7 @@ import { } from '../../utils/isBinding'; import { instrumentD1 } from './instrumentD1'; import { appendRpcMeta } from '../../utils/rpcMeta'; -import { createRpcPropagationResolver } from '../../utils/rpcPropagation'; +import { createBindingPropagationResolver, createRpcPropagationResolver } from '../../utils/bindingPropagation'; import { instrumentDurableObjectNamespace, STUB_NON_RPC_METHODS } from '../instrumentDurableObjectNamespace'; import { instrumentFetcher } from './instrumentFetcher'; import { instrumentKV } from './instrumentKV'; @@ -38,7 +38,7 @@ const instrumentedBindings = new WeakMap(); * - Workers AI (via `run` + `gateway` + `toMarkdown` duck-typing) * * @param env - The Cloudflare env object to instrument - * @param options - Optional CloudflareOptions to control RPC trace propagation + * @param options - Optional CloudflareOptions to control RPC and Queue trace propagation */ export function instrumentEnv>(env: Env, options?: CloudflareOptions): Env { if (!env || typeof env !== 'object') { @@ -46,6 +46,7 @@ export function instrumentEnv>(env: Env, opt } const shouldPropagateRpcTrace = createRpcPropagationResolver(options); + const shouldPropagateQueueTrace = createBindingPropagationResolver(options?.queueTracePropagationBindings); return new Proxy(env, { get(target, prop, receiver) { @@ -69,7 +70,7 @@ export function instrumentEnv>(env: Env, opt if (isQueue(item)) { const bindingName = typeof prop === 'string' ? prop : String(prop); - const instrumented = instrumentQueueProducer(item, bindingName); + const instrumented = instrumentQueueProducer(item, bindingName, shouldPropagateQueueTrace(bindingName)); instrumentedBindings.set(item, instrumented); return instrumented; } diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts index 34e2d8bddd7c..f6339248726f 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts @@ -1,7 +1,9 @@ import type { MessageBatch } from '@cloudflare/workers-types'; import type { AnyExportedHandler } from '../../types'; +import { TraceFlags } from '@opentelemetry/api'; import type { env as cloudflareEnv, WorkerEntrypoint } from 'cloudflare:workers'; import { + SENTRY_LINK_TYPE, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN, @@ -14,6 +16,7 @@ import { MESSAGING_SYSTEM, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS } from '@sentry/conventions/op'; +import type { SpanLink } from '@sentry/core'; import { captureException, startSpan, withIsolationScope } from '@sentry/core'; import type { CloudflareOptions } from '../../client'; import { flushAndDispose } from '../../flush'; @@ -23,8 +26,41 @@ import { addCloudResourceContext } from '../../scope-utils'; import { init } from '../../sdk'; import { instrumentContext } from '../../utils/instrumentContext'; import { setInvocationState } from '../../utils/invocationContext'; +import { extractQueueTraceContext } from '../../utils/queueTraceMeta'; import { instrumentEnv } from './instrumentEnv'; +function extractQueueTraceLinks(batch: MessageBatch): SpanLink[] { + const linkedSpans = new Set(); + const links: SpanLink[] = []; + + for (const message of batch.messages) { + const producerContext = extractQueueTraceContext(message.body); + if (!producerContext) { + continue; + } + + // `sendBatch` stamps every message with the same publish span, so link each producer only once. + const spanKey = `${producerContext.traceId}-${producerContext.spanId}`; + if (linkedSpans.has(spanKey)) { + continue; + } + linkedSpans.add(spanKey); + + links.push({ + context: { + traceId: producerContext.traceId, + spanId: producerContext.spanId, + traceFlags: producerContext.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE, + }, + attributes: { + [SENTRY_LINK_TYPE]: 'message_origin', + }, + }); + } + + return links; +} + /** * Core queue handler logic - wraps execution with Sentry instrumentation. */ @@ -47,6 +83,9 @@ function wrapQueueHandler( return startSpan( { name: `process ${batch.queue}`, + // A batch can mix messages from unrelated traces, so continuing any one of them would invent + // a false parent. Keep queue.process as a root span and link every producer instead. + links: extractQueueTraceLinks(batch), attributes: { [SENTRY_OP]: QUEUE_PROCESS, [FAAS_TRIGGER]: 'pubsub', diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts index 804cf3119d51..e2bfae75a4f7 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts @@ -10,7 +10,8 @@ import { MESSAGING_SYSTEM, } from '@sentry/conventions/attributes'; import { QUEUE_PUBLISH } from '@sentry/conventions/op'; -import { startSpan } from '@sentry/core'; +import { getTraceData, startSpan, type Span } from '@sentry/core'; +import { addQueueTraceContext } from '../../utils/queueTraceMeta'; const ORIGIN = 'auto.faas.cloudflare.queue'; @@ -20,7 +21,7 @@ function startPublishSpan( bodySize: number | undefined; messageCount?: number; }, - callback: () => T, + callback: (span: Span) => T, ): T { const { bindingName, bodySize, messageCount } = options; @@ -66,6 +67,16 @@ function getBodySize(body: unknown): number | undefined { } } +function getBatchBodySize(messages: MessageSendRequest[]): number | undefined { + return messages.reduce((acc, message) => { + const size = getBodySize(message.body); + if (size === undefined) { + return acc; + } + return (acc ?? 0) + size; + }, undefined); +} + /** * Wraps a Queue producer binding to create `queue.publish` spans on * `send` and `sendBatch` calls. @@ -73,16 +84,30 @@ function getBodySize(body: unknown): number | undefined { * The queue's own name is not available on the binding object, so we use * the env binding key (e.g. `MY_QUEUE`) as `messaging.destination.name`. */ -export function instrumentQueueProducer(queue: T, bindingName: string): T { +export function instrumentQueueProducer( + queue: T, + bindingName: string, + enableTracePropagation = false, +): T { return new Proxy(queue, { get(target, prop, receiver) { if (prop === 'send') { const original = Reflect.get(target, prop, receiver) as Queue['send']; return function (this: unknown, message: unknown, options?: QueueSendOptions): ReturnType { - return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () => - Reflect.apply(original, target, [message, options]), - ); + if (!enableTracePropagation) { + return startPublishSpan({ bindingName, bodySize: getBodySize(message) }, () => + Reflect.apply(original, target, [message, options]), + ); + } + + // The body size is only known once the trace context is injected, which must happen inside + // the span so the carrier points to queue.publish rather than its parent. + return startPublishSpan({ bindingName, bodySize: undefined }, span => { + const tracedMessage = addQueueTraceContext(message, getTraceData()['sentry-trace']); + span.setAttribute(MESSAGING_MESSAGE_BODY_SIZE, getBodySize(tracedMessage)); + return Reflect.apply(original, target, [tracedMessage, options]); + }); }; } @@ -94,17 +119,25 @@ export function instrumentQueueProducer(queue: T, bindingName: options?: QueueSendBatchOptions, ): ReturnType { const messageArray = Array.from(messages); - const totalBodySize = messageArray.reduce((acc, m) => { - const size = getBodySize(m.body); - if (size === undefined) { - return acc; - } - return (acc ?? 0) + size; - }, undefined); - - return startPublishSpan({ bindingName, bodySize: totalBodySize, messageCount: messageArray.length }, () => - Reflect.apply(original, target, [messageArray, options]), - ); + const messageCount = messageArray.length; + + if (!enableTracePropagation) { + return startPublishSpan({ bindingName, bodySize: getBatchBodySize(messageArray), messageCount }, () => + Reflect.apply(original, target, [messageArray, options]), + ); + } + + return startPublishSpan({ bindingName, bodySize: undefined, messageCount }, span => { + // Every message is published by this one span, so they all share its trace context. + const sentryTrace = getTraceData()['sentry-trace']; + const tracedMessages = messageArray.map(message => ({ + ...message, + body: addQueueTraceContext(message.body, sentryTrace), + })); + + span.setAttribute(MESSAGING_MESSAGE_BODY_SIZE, getBatchBodySize(tracedMessages)); + return Reflect.apply(original, target, [tracedMessages, options]); + }); }; } diff --git a/packages/cloudflare/src/utils/rpcPropagation.ts b/packages/cloudflare/src/utils/bindingPropagation.ts similarity index 65% rename from packages/cloudflare/src/utils/rpcPropagation.ts rename to packages/cloudflare/src/utils/bindingPropagation.ts index d914b21032a9..5d3f38f5e264 100644 --- a/packages/cloudflare/src/utils/rpcPropagation.ts +++ b/packages/cloudflare/src/utils/bindingPropagation.ts @@ -1,18 +1,14 @@ -import { matchesTracePropagationTargets } from '@sentry/core'; +import { matchesTracePropagationTargets, type TracePropagationTargets } from '@sentry/core'; import type { CloudflareOptions } from '../client'; const PROPAGATE_TO_NONE = () => false; /** - * Builds the per-binding predicate that decides whether a binding takes part in RPC trace - * propagation. - * - * Callers only. Receivers continue an incoming trace whenever one arrives, so they have nothing to - * match against. + * Builds the per-binding predicate that decides whether a binding takes part in trace propagation. */ -export function createRpcPropagationResolver(options: CloudflareOptions | undefined): (bindingName: string) => boolean { - const bindings = options?.rpcTracePropagationBindings; - +export function createBindingPropagationResolver( + bindings: TracePropagationTargets | undefined, +): (bindingName: string) => boolean { if (!bindings?.length) { return PROPAGATE_TO_NONE; } @@ -21,3 +17,14 @@ export function createRpcPropagationResolver(options: CloudflareOptions | undefi // propagation for a binding named `MY_DB`. Regular expressions still give pattern matching. return (bindingName: string) => matchesTracePropagationTargets(bindingName, bindings, true); } + +/** + * Builds the per-binding predicate that decides whether a binding takes part in RPC trace + * propagation. + * + * Callers only. Receivers continue an incoming trace whenever one arrives, so they have nothing to + * match against. + */ +export function createRpcPropagationResolver(options: CloudflareOptions | undefined): (bindingName: string) => boolean { + return createBindingPropagationResolver(options?.rpcTracePropagationBindings); +} diff --git a/packages/cloudflare/src/utils/queueTraceMeta.ts b/packages/cloudflare/src/utils/queueTraceMeta.ts new file mode 100644 index 000000000000..c63fcfa04480 --- /dev/null +++ b/packages/cloudflare/src/utils/queueTraceMeta.ts @@ -0,0 +1,63 @@ +import { extractTraceparentData } from '@sentry/core'; + +const SENTRY_QUEUE_TRACE_KEY = '__sentry_queue_trace__'; + +function isQueueTraceCarrier(body: unknown): body is Record { + if (typeof body !== 'object' || body === null || Array.isArray(body)) { + return false; + } + + // Cloudflare Queues has no message-header API. Only record-like bodies can carry a named field; binary data and + // other structured-clone values must pass through untouched. + const prototype = Object.getPrototypeOf(body); + return prototype === Object.prototype || prototype === null; +} + +export function addQueueTraceContext(body: unknown, sentryTrace: string | undefined): unknown { + if ( + !isQueueTraceCarrier(body) || + !sentryTrace || + Object.prototype.hasOwnProperty.call(body, SENTRY_QUEUE_TRACE_KEY) + ) { + return body; + } + + // Cloudflare rejects messages over 128 KB (https://developers.cloudflare.com/queues/platform/limits/), + // and the field adds about 80 bytes, so a payload right under the limit can start failing to send. + // Span links only need `sentry-trace`, so baggage would only cost more of that budget. Copy instead + // of mutating, because callers may reuse a payload after enqueueing it. + return { ...body, [SENTRY_QUEUE_TRACE_KEY]: sentryTrace }; +} + +export function extractQueueTraceContext( + body: unknown, +): { traceId: string; spanId: string; sampled: boolean | undefined } | undefined { + if (!isQueueTraceCarrier(body)) { + return undefined; + } + + if (!Object.prototype.hasOwnProperty.call(body, SENTRY_QUEUE_TRACE_KEY)) { + return undefined; + } + + const sentryTrace = body[SENTRY_QUEUE_TRACE_KEY]; + + // The key is reserved transport metadata, so strip it even if its value is not a valid trace. + // Unlike `delete`, this returns false instead of throwing on frozen bodies, which keep the field. + Reflect.deleteProperty(body, SENTRY_QUEUE_TRACE_KEY); + + if (typeof sentryTrace !== 'string') { + return undefined; + } + + const traceparentData = extractTraceparentData(sentryTrace); + if (!traceparentData?.traceId || !traceparentData.parentSpanId) { + return undefined; + } + + return { + traceId: traceparentData.traceId, + spanId: traceparentData.parentSpanId, + sampled: traceparentData.parentSampled, + }; +} diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index f4869a38615e..0d31267e8176 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -11,7 +11,15 @@ import { } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import type { PropagationContext, Scope } from '@sentry/core'; -import { captureException, flush, getCurrentScope, startSpan, withIsolationScope, withScope } from '@sentry/core'; +import { + captureException, + flush, + getCurrentScope, + isError, + startSpan, + withIsolationScope, + withScope, +} from '@sentry/core'; import type { WorkflowDelayDuration, WorkflowEntrypoint, @@ -70,6 +78,14 @@ async function propagationContextFromInstanceId(instanceId: string): Promise waitUntil?.(flushAndDispose(client))); + } + addCloudResourceContext(isolationScope); const attributes: SpanAttributes = {}; if (request.cf) { diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index 6f7e2f47744a..d917170b4745 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -274,6 +274,27 @@ describe('instrumentDurableObjectWithSentry', () => { // The only scope that all later invocations share is the default isolation scope of the isolate, so // data set in the constructor stays in the constructor's own scope. + it.each([ + [undefined, true], + [false, false], + ])('with cacheClient: %s, has a client in the constructor: %s', (cacheClient, hasClient) => { + let constructorClient: unknown; + const testClass = class { + constructor() { + constructorClient = SentryCore.getClient(); + } + }; + Reflect.construct( + instrumentDurableObjectWithSentry( + () => ({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient }), + testClass as any, + ), + [{ waitUntil: vi.fn() }, {}], + ); + + expect(constructorClient !== undefined).toBe(hasClient); + }); + it('does not apply scope data set in the constructor to later invocations', async () => { const events: Event[] = []; const waits: Promise[] = []; @@ -633,6 +654,45 @@ describe('instrumentDurableObjectWithSentry', () => { expect(frameworkDispatch.has(FrameworkLike.prototype.greet)).toBe(true); expect(second.greet('World')).toBe('Hello, World!'); }); + + // The `agents` SDK wraps `@callable()` methods before storage initialization, which can throw + // (at a storage quota limit, for example). The retry must still see the framework's wrapper as managed. + it('keeps excluding a framework-managed method when the first construction throws', () => { + const frameworkDispatch = new WeakSet(); + let failConstruction = true; + + class FrameworkLike { + constructor() { + const original = FrameworkLike.prototype.greet; + + if (!frameworkDispatch.has(original)) { + const dispatched = function (this: FrameworkLike, name: string): string { + return original.call(this, name); + }; + frameworkDispatch.add(dispatched); + FrameworkLike.prototype.greet = dispatched; + } + + if (failConstruction) { + throw new Error('storage initialization failed'); + } + } + + greet(name: string): string { + return `Hello, ${name}!`; + } + } + + const instrumented = instrumentDurableObjectWithSentry(vi.fn().mockReturnValue({}), FrameworkLike as any); + + expect(() => Reflect.construct(instrumented, [])).toThrow('storage initialization failed'); + + failConstruction = false; + const obj = Reflect.construct(instrumented, []) as FrameworkLike; + + expect(frameworkDispatch.has(FrameworkLike.prototype.greet)).toBe(true); + expect(obj.greet('World')).toBe('Hello, World!'); + }); }); // The wrapper replaces a method on a class the user owns, so it has to keep the parts of the diff --git a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts index b5858d087008..fe1fe59ce1f4 100644 --- a/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts +++ b/packages/cloudflare/test/instrumentations/instrumentEnv.test.ts @@ -223,6 +223,34 @@ describe('instrumentEnv', () => { expect(send.mock.calls[0]?.[0]).toBe('hello'); }); + it('propagates Queue trace context only to the configured bindings', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + baggage: 'sentry-release=1.0.0', + }); + const billingSend = vi.fn().mockResolvedValue(undefined); + const otherSend = vi.fn().mockResolvedValue(undefined); + const instrumented = instrumentEnv( + { + BILLING_QUEUE: { send: billingSend, sendBatch: vi.fn() }, + OTHER_QUEUE: { send: otherSend, sendBatch: vi.fn() }, + }, + { queueTracePropagationBindings: ['billing_queue'] }, + ); + + await instrumented.BILLING_QUEUE.send({ invoiceId: 'inv_123' }); + await instrumented.OTHER_QUEUE.send({ invoiceId: 'inv_456' }); + + expect(billingSend).toHaveBeenCalledExactlyOnceWith( + { + invoiceId: 'inv_123', + __sentry_queue_trace__: '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }, + undefined, + ); + expect(otherSend).toHaveBeenCalledExactlyOnceWith({ invoiceId: 'inv_456' }, undefined); + }); + it('wraps Workers KV bindings in a proxy', async () => { const get = vi.fn().mockResolvedValue('value'); const put = vi.fn().mockResolvedValue(undefined); diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts index afe51790adfd..d58d1e5ff4fa 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentQueue.test.ts @@ -2,6 +2,7 @@ // Although this is not ideal, this is the best we can do until we have a better way to test cloudflare workers. import type { ExecutionContext, MessageBatch } from '@cloudflare/workers-types'; +import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import type { Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest'; @@ -295,6 +296,108 @@ describe('instrumentQueue', () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); }); + + test('links producer traces and removes transport context before invoking the handler', async () => { + const originalBatch = createMockQueueBatch(); + const batch = { + ...originalBatch, + messages: [ + { + ...originalBatch.messages[0]!, + body: { + invoiceId: 'inv_123', + __sentry_queue_trace__: '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }, + }, + { + ...originalBatch.messages[1]!, + body: { + invoiceId: 'inv_456', + __sentry_queue_trace__: 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-0', + }, + }, + { + ...originalBatch.messages[0]!, + id: '3', + body: { + invoiceId: 'inv_789', + __sentry_queue_trace__: '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }, + }, + { + ...originalBatch.messages[1]!, + id: '4', + body: { + invoiceId: 'inv_invalid', + __sentry_queue_trace__: 'not-a-valid-trace', + }, + }, + ], + } satisfies MessageBatch; + let receivedBodies: unknown[] = []; + let processSpan: ReturnType | undefined; + const handler = { + queue(receivedBatch, _env, _context) { + receivedBodies = receivedBatch.messages.map(message => message.body); + processSpan = SentryCore.spanToJSON(SentryCore.getActiveSpan()!); + }, + } satisfies ExportedHandler; + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1 }), handler); + + await wrappedHandler.queue?.(batch, MOCK_ENV, createMockExecutionContext()); + + expect(receivedBodies).toEqual([ + { invoiceId: 'inv_123' }, + { invoiceId: 'inv_456' }, + { invoiceId: 'inv_789' }, + { invoiceId: 'inv_invalid' }, + ]); + expect(processSpan?.parent_span_id).toBeUndefined(); + expect(processSpan?.links).toEqual([ + { + trace_id: '1234567890abcdef1234567890abcdef', + span_id: '1234567890abcdef', + sampled: true, + attributes: { [SENTRY_LINK_TYPE]: 'message_origin' }, + }, + { + trace_id: 'abcdefabcdefabcdefabcdefabcdefab', + span_id: 'fedcbafedcbafedc', + sampled: false, + attributes: { [SENTRY_LINK_TYPE]: 'message_origin' }, + }, + ]); + }); + + test('does not throw when the transport context cannot be removed from a frozen body', async () => { + const originalBatch = createMockQueueBatch(); + const body = Object.freeze({ + invoiceId: 'inv_123', + __sentry_queue_trace__: '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }); + const batch = { + ...originalBatch, + messages: [{ ...originalBatch.messages[0]!, body }], + } satisfies MessageBatch; + let processSpan: ReturnType | undefined; + const handler = { + queue(_batch, _env, _context) { + processSpan = SentryCore.spanToJSON(SentryCore.getActiveSpan()!); + }, + } satisfies ExportedHandler; + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1 }), handler); + + await wrappedHandler.queue?.(batch, MOCK_ENV, createMockExecutionContext()); + + expect(processSpan?.links).toEqual([ + { + trace_id: '1234567890abcdef1234567890abcdef', + span_id: '1234567890abcdef', + sampled: true, + attributes: { [SENTRY_LINK_TYPE]: 'message_origin' }, + }, + ]); + }); }); test('flush must be called when all waitUntil are done', async () => { diff --git a/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts b/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts index f176d92e53db..bdf85eab1b93 100644 --- a/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts +++ b/packages/cloudflare/test/instrumentations/worker/instrumentQueueProducer.test.ts @@ -1,4 +1,5 @@ import type { Queue } from '@cloudflare/workers-types'; +import { MESSAGING_MESSAGE_BODY_SIZE } from '@sentry/conventions/attributes'; import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { instrumentQueueProducer } from '../../../src/instrumentations/worker/instrumentQueueProducer'; @@ -12,6 +13,7 @@ function createMockQueue(): Queue { describe('instrumentQueueProducer', () => { beforeEach(() => { + vi.restoreAllMocks(); vi.clearAllMocks(); }); @@ -26,6 +28,110 @@ describe('instrumentQueueProducer', () => { expect(queue.send).toHaveBeenLastCalledWith({ hello: 'world' }, { contentType: 'json' }); }); + test('does not add trace context by default', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE'); + const message = { invoiceId: 'inv_123' }; + + await wrapped.send(message); + + expect(queue.send).toHaveBeenCalledTimes(1); + expect(queue.send).toHaveBeenLastCalledWith(message, undefined); + }); + + test('adds trace context to object payloads', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + baggage: 'sentry-release=1.0.0', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + const message = { invoiceId: 'inv_123' }; + + await wrapped.send(message); + + expect(queue.send).toHaveBeenCalledTimes(1); + expect(queue.send).toHaveBeenLastCalledWith( + { + invoiceId: 'inv_123', + __sentry_queue_trace__: '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }, + undefined, + ); + expect(message).toEqual({ invoiceId: 'inv_123' }); + }); + + test('includes injected trace context in body size', async () => { + const setAttribute = vi.fn(); + vi.spyOn(SentryCore, 'startSpan').mockImplementation(((_options, callback) => + callback({ setAttribute } as unknown as SentryCore.Span)) as typeof SentryCore.startSpan); + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + + await wrapped.send({ invoiceId: 'inv_123' }); + + const sentBody = { + invoiceId: 'inv_123', + __sentry_queue_trace__: '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }; + expect(queue.send).toHaveBeenCalledExactlyOnceWith(sentBody, undefined); + expect(setAttribute).toHaveBeenCalledExactlyOnceWith( + MESSAGING_MESSAGE_BODY_SIZE, + new TextEncoder().encode(JSON.stringify(sentBody)).byteLength, + ); + }); + + test('preserves an existing metadata field', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + const message = { + invoiceId: 'inv_123', + __sentry_queue_trace__: 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-0', + }; + + await wrapped.send(message); + + expect(queue.send).toHaveBeenCalledTimes(1); + expect(queue.send).toHaveBeenLastCalledWith(message, undefined); + }); + + test('does not add trace context to array payloads', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + const message = [{ invoiceId: 'inv_123' }]; + + await wrapped.send(message); + + expect(queue.send).toHaveBeenCalledTimes(1); + expect(queue.send).toHaveBeenLastCalledWith(message, undefined); + }); + + test('does not add trace context to binary payloads', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '1234567890abcdef1234567890abcdef-1234567890abcdef-1', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + const message = new ArrayBuffer(42); + + await wrapped.send(message); + + expect(queue.send).toHaveBeenCalledTimes(1); + expect(queue.send).toHaveBeenLastCalledWith(message, undefined); + }); + test('starts a queue.publish span with messaging attributes', async () => { const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); const queue = createMockQueue(); @@ -97,6 +203,69 @@ describe('instrumentQueueProducer', () => { expect(queue.sendBatch).toHaveBeenCalledTimes(1); }); + test('adds the publish span context to every object body', async () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-0', + baggage: 'sentry-environment=production', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + + await wrapped.sendBatch( + [ + { body: { invoiceId: 'inv_123' }, contentType: 'json' }, + { body: { invoiceId: 'inv_456' }, delaySeconds: 10 }, + ], + { delaySeconds: 5 }, + ); + + expect(queue.sendBatch).toHaveBeenCalledTimes(1); + expect(queue.sendBatch).toHaveBeenLastCalledWith( + [ + { + body: { + invoiceId: 'inv_123', + __sentry_queue_trace__: 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-0', + }, + contentType: 'json', + }, + { + body: { + invoiceId: 'inv_456', + __sentry_queue_trace__: 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-0', + }, + delaySeconds: 10, + }, + ], + { delaySeconds: 5 }, + ); + }); + + test('includes injected trace context in batch body size', async () => { + const setAttribute = vi.fn(); + vi.spyOn(SentryCore, 'startSpan').mockImplementation(((_options, callback) => + callback({ setAttribute } as unknown as SentryCore.Span)) as typeof SentryCore.startSpan); + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-1', + }); + const queue = createMockQueue(); + const wrapped = instrumentQueueProducer(queue, 'BILLING_QUEUE', true); + + await wrapped.sendBatch([{ body: { invoiceId: 'inv_123' } }, { body: { invoiceId: 'inv_456' } }]); + + const sentTrace = 'abcdefabcdefabcdefabcdefabcdefab-fedcbafedcbafedc-1'; + const sentMessages = [ + { body: { invoiceId: 'inv_123', __sentry_queue_trace__: sentTrace } }, + { body: { invoiceId: 'inv_456', __sentry_queue_trace__: sentTrace } }, + ]; + const expectedSize = sentMessages.reduce( + (total, message) => total + new TextEncoder().encode(JSON.stringify(message.body)).byteLength, + 0, + ); + expect(queue.sendBatch).toHaveBeenCalledExactlyOnceWith(sentMessages, undefined); + expect(setAttribute).toHaveBeenCalledExactlyOnceWith(MESSAGING_MESSAGE_BODY_SIZE, expectedSize); + }); + test('starts a queue.publish span with batch attributes', async () => { const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); const queue = createMockQueue(); diff --git a/packages/cloudflare/test/utils/rpcPropagation.test.ts b/packages/cloudflare/test/utils/bindingPropagation.test.ts similarity index 96% rename from packages/cloudflare/test/utils/rpcPropagation.test.ts rename to packages/cloudflare/test/utils/bindingPropagation.test.ts index 047424e442ed..01840c3c2200 100644 --- a/packages/cloudflare/test/utils/rpcPropagation.test.ts +++ b/packages/cloudflare/test/utils/bindingPropagation.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createRpcPropagationResolver } from '../../src/utils/rpcPropagation'; +import { createRpcPropagationResolver } from '../../src/utils/bindingPropagation'; describe('createRpcPropagationResolver', () => { it('propagates to nothing when no options are available', () => { diff --git a/packages/cloudflare/test/workflow.test.ts b/packages/cloudflare/test/workflow.test.ts index 2dc5e1ceb02d..3490b51f8272 100644 --- a/packages/cloudflare/test/workflow.test.ts +++ b/packages/cloudflare/test/workflow.test.ts @@ -616,6 +616,111 @@ describe.skipIf(NODE_MAJOR_VERSION < 20)('workflows', () => { }); }); + describe('NonRetryableError before the final attempt', () => { + class NonRetryableError extends Error { + constructor(message: string, name = 'NonRetryableError') { + super(message); + this.name = name; + } + } + + async function runFailingWorkflow(createError: () => Error): Promise<{ errorEvents: any[]; attempts: number }> { + const retryLimit = 2; + let attempts = 0; + const step: WorkflowStep = { + do: vi + .fn() + .mockImplementation( + async ( + _name: string, + configOrCallback: WorkflowStepConfig | ((...args: unknown[]) => Promise), + maybeCallback?: (...args: unknown[]) => Promise, + ) => { + const callback = (typeof configOrCallback === 'function' ? configOrCallback : maybeCallback)!; + let lastError: unknown; + for (let attempt = 1; attempt <= retryLimit + 1; attempt++) { + attempts = attempt; + try { + return await callback({ attempt, config: { retries: { limit: retryLimit }, timeout: 60000 } }); + } catch (err) { + lastError = err; + // The engine's rule for ending a step without further retries + const { name, message } = err as Error; + if (name === 'NonRetryableError' || message.startsWith('NonRetryableError')) { + break; + } + } + } + throw lastError; + }, + ), + sleep: vi.fn(), + sleepUntil: vi.fn(), + waitForEvent: vi.fn(), + }; + + class FailingWorkflow { + constructor(_ctx: ExecutionContext, _env: unknown) {} + + async run(_event: Readonly>, step: WorkflowStep): Promise { + await step.do('failing step', async () => { + throw createError(); + }); + } + } + + const TestWorkflowInstrumented = instrumentWorkflowWithSentry(() => getSentryOptions(), FailingWorkflow as any); + const workflow = new TestWorkflowInstrumented(mockContext, {}) as FailingWorkflow; + const event = { payload: {}, timestamp: new Date(), instanceId: INSTANCE_ID }; + await expect(workflow.run(event, step)).rejects.toThrow('permanent failure'); + + const errorEvents = mockTransport.send.mock.calls + .map(call => (call[0] as any)[1] as any[]) + .flatMap(items => items.filter(i => i[0].type === 'event').map(i => i[1])); + + return { errorEvents, attempts }; + } + + test('captures a NonRetryableError thrown on the first attempt', async () => { + const { errorEvents, attempts } = await runFailingWorkflow(() => new NonRetryableError('permanent failure')); + + expect(attempts).toBe(1); + expect(errorEvents).toHaveLength(1); + expect(errorEvents[0]).toMatchObject({ + exception: { + values: [ + expect.objectContaining({ + type: 'NonRetryableError', + value: 'permanent failure', + mechanism: { type: 'auto.faas.cloudflare.workflow', handled: true }, + }), + ], + }, + }); + }); + + test('captures an error whose message starts with "NonRetryableError" on the first attempt', async () => { + const { errorEvents, attempts } = await runFailingWorkflow( + () => new Error('NonRetryableError: permanent failure'), + ); + + expect(attempts).toBe(1); + expect(errorEvents).toHaveLength(1); + }); + + test('captures a NonRetryableError with a custom name only on the final attempt, as the engine retries it', async () => { + const { errorEvents, attempts } = await runFailingWorkflow( + () => new NonRetryableError('permanent failure', 'MyError'), + ); + + expect(attempts).toBe(3); + expect(errorEvents).toHaveLength(1); + expect(errorEvents[0]).toMatchObject({ + exception: { values: [expect.objectContaining({ type: 'MyError', value: 'permanent failure' })] }, + }); + }); + }); + test('emits error per attempt on older Cloudflare without step context', async () => { // Mock that always throws — simulates a step that fails through every retry // Does NOT pass step context (simulates older Cloudflare versions) diff --git a/packages/core/package.json b/packages/core/package.json index faaace218f88..f01c6b16c80e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -75,9 +75,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-core-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-core-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", @@ -92,6 +92,6 @@ "zod": "^4.6.7" }, "dependencies": { - "@sentry/conventions": "^0.30.0" + "@sentry/conventions": "^0.32.0" } } diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index 49eaae2b7916..1bf9505714f1 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -256,6 +256,13 @@ interface SourceMapsOptions { * When unset, the plugin removes only the source maps it turned on itself, that is, when the bundler's source map * setting was not set. `true` also removes source maps you enabled yourself. `false` never removes them; the plugin * still turns on hidden source maps when the setting is not set, so they can be uploaded. + * + * When the plugin turned source maps on, none of them reach the build output, not even those of chunks that + * `sourcemaps.assets` or `sourcemaps.ignore` keep out of the upload. Source maps you enabled yourself are only + * removed for the JavaScript chunks that were uploaded; excluded chunks keep theirs. + * + * With webpack's array form of `devtool`, `true` replaces the array with one hidden source map setting for + * all asset types, because webpack hides the array from plugins. */ deleteSourcemapsAfterUpload?: boolean; diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts index cf48f39c7f4d..fe6954fdff1c 100644 --- a/packages/core/src/carrier.ts +++ b/packages/core/src/carrier.ts @@ -5,6 +5,7 @@ import type { Scope } from './scope'; import type { SegmentSpanCaptureStrategy } from './tracing/segmentSpanCaptureStrategy'; import type { SerializedLog } from './types/log'; import type { SerializedMetric } from './types/metric'; +import type { TunnelStore } from './utils/tunnelStore'; import { SDK_VERSION } from './utils/version'; import { GLOBAL_OBJ } from './utils/worldwide'; @@ -50,6 +51,9 @@ export interface SentryCarrier { encodePolyfill?: (input: string) => Uint8Array; /** Overwrites TextDecoder used in `@sentry/core`, need for `react-native@0.73` and older */ decodePolyfill?: (input: Uint8Array) => string; + + /** Only `utils/tunnelStore.ts` reads or writes it. */ + tunnelStore?: TunnelStore; } /** diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 54f0c2dae0d2..e9f8b0e31404 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -6,8 +6,6 @@ import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import type { IntegrationIndex } from './integration'; import { afterSetupIntegrations, setupIntegration, setupIntegrations } from './integration'; -import { _INTERNAL_flushLogsBuffer } from './logs/internal'; -import { _INTERNAL_flushMetricsBuffer } from './metrics/internal'; import type { Scope } from './scope'; import { updateSession } from './session'; import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; @@ -26,7 +24,6 @@ import type { FeedbackEvent } from './types/feedback'; import type { Integration } from './types/integration'; import type { Log } from './types/log'; import type { Metric } from './types/metric'; -import type { Primitive } from './types/misc'; import type { ClientOptions } from './types/options'; import type { ParameterizedString } from './types/parameterize'; import type { ReplayEndEvent, ReplayStartEvent } from './types/replay'; @@ -53,7 +50,6 @@ import { safeMathRandom } from './utils/randomSafeContext'; import { safeCallback } from './utils/safeCallback'; import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; import { rejectedSyncPromise } from './utils/syncpromise'; -import { safeUnref } from './utils/timer'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; import { maybeWarnAboutIgnoredTransactionOptions } from './utils/warnAboutIgnoredTransactionOptions'; import { resolveDataCollectionOptions } from './utils/data-collection/resolveDataCollectionOptions'; @@ -65,9 +61,6 @@ const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing const INTERNAL_ERROR_SYMBOL = Symbol.for('SentryInternalError'); const DO_NOT_SEND_EVENT_SYMBOL = Symbol.for('SentryDoNotSendEventError'); -// Default interval for flushing logs and metrics (5 seconds) -const DEFAULT_FLUSH_INTERVAL = 5000; - // Clones of a scope keep their own reference to its client, so unbinding a // closed client from one scope does not reach the others. The mark lives on // the client under a `Symbol.for` key, so every copy of the SDK can read it. @@ -120,70 +113,6 @@ function _isDoNotSendEventError(error: unknown): error is DoNotSendEventError { return isObjectLike(error) && DO_NOT_SEND_EVENT_SYMBOL in error; } -/** - * Sets up weight-based flushing for logs or metrics. - * This helper function encapsulates the common pattern of: - * 1. Tracking accumulated weight of items - * 2. Flushing when weight exceeds threshold (800KB) - * 3. Flushing after timeout period from the first item - * - * Uses closure variables to track weight and timeout state. - */ -function setupWeightBasedFlushing< - T, - AfterCaptureHook extends 'afterCaptureLog' | 'afterCaptureMetric', - FlushHook extends 'flushLogs' | 'flushMetrics', ->( - client: Client, - afterCaptureHook: AfterCaptureHook, - flushHook: FlushHook, - estimateSizeFn: (item: T) => number, - flushFn: (client: Client) => void, -): void { - // Track weight and timeout in closure variables - let weight = 0; - let flushTimeout: ReturnType | undefined; - let isTimerActive = false; - - // @ts-expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe - client.on(flushHook, () => { - weight = 0; - clearTimeout(flushTimeout); - isTimerActive = false; - }); - - // @ts-expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe - client.on(afterCaptureHook, (item: T) => { - weight += estimateSizeFn(item); - - // We flush the buffer if it exceeds 0.8 MB - // The weight is a rough estimate, so we flush way before the payload gets too big. - if (weight >= 800_000) { - flushFn(client); - } else if (!isTimerActive) { - const flushInterval = client.getOptions()._flushInterval ?? DEFAULT_FLUSH_INTERVAL; - if (flushInterval > 0) { - // Only start timer if one isn't already running. - // This prevents flushing being delayed by items that arrive close to the timeout limit - // and thus resetting the flushing timeout and delaying items being flushed. - isTimerActive = true; - // Use safeUnref so the timer doesn't prevent the process from exiting - flushTimeout = safeUnref( - setTimeout(() => { - flushFn(client); - // Note: isTimerActive is reset by the flushHook handler above, not here, - // to avoid race conditions when new items arrive during the flush. - }, flushInterval), - ); - } - } - }); - - client.on('flush', () => { - flushFn(client); - }); -} - /** * Base implementation for all JavaScript SDK clients. * @@ -313,18 +242,6 @@ export abstract class Client { url, }); } - - // Setup log flushing with weight and timeout tracking - setupWeightBasedFlushing(this, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer); - - // Setup metric flushing with weight and timeout tracking - setupWeightBasedFlushing( - this, - 'afterCaptureMetric', - 'flushMetrics', - estimateMetricSizeInBytes, - _INTERNAL_flushMetricsBuffer, - ); } /** @@ -1874,79 +1791,3 @@ function isErrorEvent(event: Event): event is ErrorEvent { function isTransactionEvent(event: Event): event is TransactionEvent { return event.type === 'transaction'; } - -/** - * Estimate the size of a metric in bytes. - * - * @param metric - The metric to estimate the size of. - * @returns The estimated size of the metric in bytes. - */ -function estimateMetricSizeInBytes(metric: Metric): number { - let weight = 0; - - // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. - if (metric.name) { - weight += metric.name.length * 2; - } - - // Add weight for number - weight += 8; - - return weight + estimateAttributesSizeInBytes(metric.attributes); -} - -/** - * Estimate the size of a log in bytes. - * - * @param log - The log to estimate the size of. - * @returns The estimated size of the log in bytes. - */ -function estimateLogSizeInBytes(log: Log): number { - let weight = 0; - - // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. - if (log.message) { - weight += log.message.length * 2; - } - - return weight + estimateAttributesSizeInBytes(log.attributes); -} - -/** - * Estimate the size of attributes in bytes. - * - * @param attributes - The attributes object to estimate the size of. - * @returns The estimated size of the attributes in bytes. - */ -function estimateAttributesSizeInBytes(attributes: Record | undefined): number { - if (!attributes) { - return 0; - } - - let weight = 0; - - Object.values(attributes).forEach(value => { - if (Array.isArray(value)) { - weight += value.length * estimatePrimitiveSizeInBytes(value[0]); - } else if (isPrimitive(value)) { - weight += estimatePrimitiveSizeInBytes(value); - } else { - // For objects values, we estimate the size of the object as 100 bytes - weight += 100; - } - }); - - return weight; -} - -function estimatePrimitiveSizeInBytes(value: Primitive): number { - if (typeof value === 'string') { - return value.length * 2; - } else if (typeof value === 'number') { - return 8; - } else if (typeof value === 'boolean') { - return 4; - } - - return 0; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b81035787bfa..6efcccdc3b18 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -104,7 +104,7 @@ export { parameterize, fmt } from './utils/parameterize'; export type { HandleTunnelRequestOptions } from './utils/tunnelForward'; export { handleTunnelRequest } from './utils/tunnelForward'; export { - applyTunnelDefaults as _INTERNAL_applyTunnelDefaults, + applyBrowserTunnelDefaults as _INTERNAL_applyBrowserTunnelDefaults, getTunnelPath, normalizeTunnelBasePath as _INTERNAL_normalizeTunnelBasePath, } from './utils/tunnel'; diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index 2f3a8ce295d3..f206051512ac 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -19,22 +19,29 @@ const _functionToStringIntegration = (() => { const callOriginalFunctionToString = Function.prototype.call.bind(originalFunctionToString) as ( thisArg: unknown, ) => string; + // eslint-disable-next-line @typescript-eslint/unbound-method + const hasOwnProperty = Function.prototype.call.bind(Object.prototype.hasOwnProperty) as ( + obj: unknown, + key: PropertyKey, + ) => boolean; // intrinsics (like Function.prototype) might be immutable in some environments // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) try { Function.prototype.toString = function (this: WrappedFunction): string { - const originalFunction = getOriginalFunction(this); let unwrappedFunction: WrappedFunction | undefined; try { - if (SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined) { - unwrappedFunction = originalFunction; + // `markFunctionWrapped` sets the original as an own property. An inherited lookup could walk a + // prototype chain that loops through a Proxy and overflow the stack, which native `toString` never does. + if (SETUP_CLIENTS.has(getClient() as Client) && hasOwnProperty(this, '__sentry_original__')) { + unwrappedFunction = getOriginalFunction(this); } } catch { // Reading the Sentry carrier off `getClient()` can throw a `SecurityError` when `this` (or the global - // object) is a `WindowProxy` whose browsing context was navigated cross-origin. The native - // `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise. + // object) is a `WindowProxy` whose browsing context was navigated cross-origin, and Proxy traps on + // `this` can throw too. The native `toString` never throws here, so fall back to it to avoid turning + // harmless introspection into noise. } return callOriginalFunctionToString(unwrappedFunction ?? this); diff --git a/packages/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index af5826fb6c86..40298235066b 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -24,6 +24,8 @@ import type { HttpIncomingMessage, HttpInstrumentationOptions, HttpServer, HttpS import { DEBUG_BUILD } from '../../debug-build'; import { debug } from '../../utils/debug-logger'; import { safeCallback } from '../../utils/safeCallback'; +import { isServedTunnelRequest } from '../../utils/tunnel'; +import { getNodeTunnelHandler, setHttpServerHandlesTunnel } from '../../utils/tunnelStore'; import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from '../../currentScopes'; import { hasSpansEnabled } from '../../utils/hasSpansEnabled'; import { @@ -132,6 +134,20 @@ export function instrumentServer(options: HttpInstrumentationOptions, server: Ht return target.apply(thisArg, args); } + // Tunnel requests record no session or span. This wrapper answers them ahead of the app, so `emit` needs no + // second wrapper for the tunnel. With `intercept: false` the app answers them, so they keep their own isolation + // scope. + if (isServedTunnelRequest(request)) { + if (getNodeTunnelHandler()?.(request, response)) { + return true; + } + + const tunnelIsolationScope = getIsolationScope().clone(); + tunnelIsolationScope.setClient(client); + + return withIsolationScope(tunnelIsolationScope, () => target.apply(thisArg, args)); + } + DEBUG_BUILD && debug.log(INTEGRATION_NAME, 'Handling incoming request'); const isolationScope = getIsolationScope().clone(); isolationScope.setClient(client); @@ -241,6 +257,7 @@ export function getHttpServerSubscriptions(options: HttpInstrumentationOptions): // this because it creates spans through a separate code path). const userWrap = options.wrapServerEmitRequest; const spanWrap = buildServerSpanWrap(options); + setHttpServerHandlesTunnel(); const effectiveOptions: HttpInstrumentationOptions = { ...options, diff --git a/packages/core/src/integrations/http/types.ts b/packages/core/src/integrations/http/types.ts index 7ec38a86fd4a..49a817fbd07b 100644 --- a/packages/core/src/integrations/http/types.ts +++ b/packages/core/src/integrations/http/types.ts @@ -68,6 +68,9 @@ export interface HttpServerResponse { once(ev: string | symbol, handler: (...data: unknown[]) => void): this; once(ev: 'close', handler: () => void): this; on(ev: string | symbol, handler: (...data: unknown[]) => void): this; + headersSent?: boolean; + writeHead(statusCode: number, headers?: Record): this; + end(chunk?: Uint8Array): this; } export interface HttpServer { @@ -101,6 +104,7 @@ export interface HttpIncomingMessage { addListener(event: string | symbol, listener: (...args: unknown[]) => void): this; off(event: string | symbol, listener: (...args: unknown[]) => void): this; removeListener(event: string | symbol, listener: (...args: unknown[]) => void): this; + [Symbol.asyncIterator](): AsyncIterator; } /** Minimal interface for the Node.js `http.ClientRequest` constructor. */ diff --git a/packages/core/src/integrations/serveTunnel.ts b/packages/core/src/integrations/serveTunnel.ts index 3994b35a252d..42a618d48a58 100644 --- a/packages/core/src/integrations/serveTunnel.ts +++ b/packages/core/src/integrations/serveTunnel.ts @@ -1,14 +1,12 @@ import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types/integration'; import type { ServerTunnelOptions } from '../types/options'; -import { enableManagedTunnel } from '../utils/managedTunnelDefaults'; -import { SERVE_TUNNEL_INTEGRATION_NAME } from '../utils/tunnel'; +import { enableManagedTunnel } from '../utils/tunnelDefaults'; import { forwardTunnelRequest } from '../utils/tunnelForward'; const _serveTunnelIntegration = ((options: ServerTunnelOptions = {}) => { return { - name: SERVE_TUNNEL_INTEGRATION_NAME, - forward: forwardTunnelRequest, + name: 'ServeTunnel', setup(client) { enableManagedTunnel(client, options, forwardTunnelRequest); }, @@ -21,8 +19,8 @@ const _serveTunnelIntegration = ((options: ServerTunnelOptions = {}) => { * With this integration on the server and `tunnel: true` in the browser `init`, the browser sends its events to your * own server instead of Sentry, where ad-blockers can't drop them, and your server forwards them to Sentry. * - * Meta-framework SDKs (e.g. Next.js, Nuxt or SvelteKit) answer the tunnel requests for you. On a standalone server - * (e.g. Express or Fastify), also call `handleTunnelRequestIfMatched` in your request handling. + * The server SDKs answer the tunnel requests before the app sees them. To serve them from your own code instead, set + * `intercept: false` and call `handleTunnelRequestIfMatched`. * * By default only envelopes for the server's own DSN are forwarded. * diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 16b6f95588b7..0b6b7ab22bca 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -283,7 +283,7 @@ function instrumentAuthOperation(operation: AuthOperationFn, isAdmin = false): A // operation that internally makes the respective request to the database. // So I think we can interpret this as a "db.query.summary"-esque span name. // Either way, it's definitely low-cardinality. - // see: https://getsentry.github.io/sentry-conventions/names/#db-queries + // see: https://conventions.sentry.dev/names/#db-queries operationName : // This name makes little sense semantically but preserving it for now to // avoid a breaking change in the transaction path. Will be removed once we remove diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index 652212746a5c..dd5cc98ee5fc 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -26,6 +26,7 @@ import { getActiveSpan } from '../utils/spanUtils'; import { timestampInSeconds } from '../utils/time'; import { getSequenceAttribute } from '../utils/timestampSequence'; import { _getTraceInfoFromScope } from '../utils/trace-info'; +import { estimateLogSizeInBytes, setupWeightBasedFlushing } from '../utils/weightBasedFlushing'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; import { createLogEnvelope } from './envelope'; @@ -64,6 +65,8 @@ export function _INTERNAL_captureSerializedLog(client: Client, serializedLog: Se const logBuffer = _INTERNAL_getLogBuffer(client); if (logBuffer === undefined) { + // Set up flushing on the first log, so apps that never capture logs don't bundle the flush code. + setupWeightBasedFlushing(client, 'afterCaptureLog', 'flushLogs', estimateLogSizeInBytes, _INTERNAL_flushLogsBuffer); bufferMap.set(client, [serializedLog]); } else { if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) { diff --git a/packages/core/src/metrics/internal.ts b/packages/core/src/metrics/internal.ts index 4d0635522bd6..6e4dd5881009 100644 --- a/packages/core/src/metrics/internal.ts +++ b/packages/core/src/metrics/internal.ts @@ -24,6 +24,7 @@ import { getActiveSpan } from '../utils/spanUtils'; import { timestampInSeconds } from '../utils/time'; import { getSequenceAttribute } from '../utils/timestampSequence'; import { _getTraceInfoFromScope } from '../utils/trace-info'; +import { estimateMetricSizeInBytes, setupWeightBasedFlushing } from '../utils/weightBasedFlushing'; import { createMetricEnvelope } from './envelope'; const MAX_METRIC_BUFFER_SIZE = 1000; @@ -61,6 +62,14 @@ export function _INTERNAL_captureSerializedMetric(client: Client, serializedMetr const metricBuffer = _INTERNAL_getMetricBuffer(client); if (metricBuffer === undefined) { + // Set up flushing on the first metric, so apps that never capture metrics don't bundle the flush code. + setupWeightBasedFlushing( + client, + 'afterCaptureMetric', + 'flushMetrics', + estimateMetricSizeInBytes, + _INTERNAL_flushMetricsBuffer, + ); bufferMap.set(client, [serializedMetric]); } else { if (metricBuffer.length >= MAX_METRIC_BUFFER_SIZE) { diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index ab9729f60732..ff1376dbee5d 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -22,7 +22,7 @@ import { uuid4 } from './utils/misc'; import { makePromiseBuffer } from './utils/promisebuffer'; import { resolvedSyncPromise } from './utils/syncpromise'; import { _getTraceInfoFromScope } from './utils/trace-info'; -import { applyManagedTunnelDefaults } from './utils/managedTunnelDefaults'; +import { applyServerTunnelDefaults } from './utils/tunnelDefaults'; export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; @@ -60,7 +60,7 @@ export class ServerRuntimeClient< super(options); - applyManagedTunnelDefaults(this); + applyServerTunnelDefaults(this); this._setUpMetricsProcessing(); } diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 08c2b3e99a55..76e802fdfe67 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -8,20 +8,27 @@ export type { ServerRuntimeClientOptions } from './server-runtime-client'; export { ServerRuntimeClient } from './server-runtime-client'; export type { TunnelRequestLike } from './utils/tunnel'; export { + getServedTunnel as _INTERNAL_getServedTunnel, getTunnelPaths as _INTERNAL_getTunnelPaths, handleServedTunnelRequest as _INTERNAL_handleServedTunnelRequest, isServedTunnelRequest as _INTERNAL_isServedTunnelRequest, isTunnelRequest, } from './utils/tunnel'; +export { + httpServerHandlesTunnel as _INTERNAL_httpServerHandlesTunnel, + setNodeTunnelHandler as _INTERNAL_setNodeTunnelHandler, +} from './utils/tunnelStore'; export { forwardTunnelRequest as _INTERNAL_forwardTunnelRequest, + handleServedNodeTunnelRequest as _INTERNAL_handleServedNodeTunnelRequest, + pickForwardedHeaders as _INTERNAL_pickForwardedHeaders, handleTunnelRequestIfMatched, } from './utils/tunnelForward'; export { serveTunnelIntegration } from './integrations/serveTunnel'; export { - _INTERNAL_resetManagedTunnelDefaults, - setManagedTunnelDefaults as _INTERNAL_setManagedTunnelDefaults, -} from './utils/managedTunnelDefaults'; + _INTERNAL_resetTunnelDefaults, + setTunnelDefaults as _INTERNAL_setTunnelDefaults, +} from './utils/tunnelDefaults'; export type { ServerRuntimeOptions } from './types/options'; export { trpcMiddleware } from './trpc'; export { wrapMcpServerWithSentry } from './integrations/mcp-server'; diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 41c3a0257de3..4e209c562723 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -588,7 +588,7 @@ function sendStandaloneSpan(span: SentrySpan, client: Client): void { return; } - const { _segmentSpan, ...serializedSpan } = captureSpan(span, client); + const { _segmentSpan, _segmentNameAtCapture, ...serializedSpan } = captureSpan(span, client); const dsc = getDynamicSamplingContextFromSpan(_segmentSpan); // sendEnvelope should not throw // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/core/src/tracing/spans/captureSpan.ts b/packages/core/src/tracing/spans/captureSpan.ts index 886f0d95cc0e..6a5bcc8b719e 100644 --- a/packages/core/src/tracing/spans/captureSpan.ts +++ b/packages/core/src/tracing/spans/captureSpan.ts @@ -32,6 +32,7 @@ import { export type SerializedStreamedSpanWithSegmentSpan = SerializedStreamedSpan & { _segmentSpan: Span; + _segmentNameAtCapture?: string; }; /** @@ -41,7 +42,7 @@ export type SerializedStreamedSpanWithSegmentSpan = SerializedStreamedSpan & { * to an already-ended OTel span instance (which is blocked by the OTel Span class). * * @returns the final serialized span with a reference to its segment span. This reference - * is needed later on to compute the DSC for the span envelope. + * is needed later on to compute the DSC and the final segment name for the span envelope. */ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanWithSegmentSpan { // Convert to JSON FIRST - we cannot write to an already-ended span @@ -88,6 +89,7 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW return { ...streamedSpanJsonToSerializedSpan(processedSpan), _segmentSpan: segmentSpan, + _segmentNameAtCapture: serializedSegmentSpan.name, }; } diff --git a/packages/core/src/tracing/spans/spanBuffer.ts b/packages/core/src/tracing/spans/spanBuffer.ts index 3066ae3b2790..fb1385799757 100644 --- a/packages/core/src/tracing/spans/spanBuffer.ts +++ b/packages/core/src/tracing/spans/spanBuffer.ts @@ -1,12 +1,14 @@ import type { Client } from '../../client'; import { DEBUG_BUILD } from '../../debug-build'; -import type { SerializedStreamedSpan } from '../../types/span'; +import type { SerializedStreamedSpan, Span } from '../../types/span'; import { debug } from '../../utils/debug-logger'; +import { spanToJSON } from '../../utils/spanUtils'; import { safeUnref } from '../../utils/timer'; import { getDynamicSamplingContextFromSpan } from '../dynamicSamplingContext'; import type { SerializedStreamedSpanWithSegmentSpan } from './captureSpan'; import { createStreamedSpanEnvelope } from './envelope'; import { estimateSerializedSpanSizeInBytes } from './estimateSize'; +import { SENTRY_SEGMENT_NAME } from '@sentry/conventions/attributes'; /** * We must not send more than 1000 spans in one envelope. @@ -168,9 +170,19 @@ export class SpanBuffer { const dsc = getDynamicSamplingContextFromSpan(segmentSpan); + const currentSegmentNames = new Map(); + const getCurrentSegmentName = (span: Span): string => { + let name = currentSegmentNames.get(span); + if (name === undefined) { + name = spanToJSON(span).name; + currentSegmentNames.set(span, name); + } + return name; + }; + const cleanedSpans: SerializedStreamedSpan[] = spans.map(spanJSON => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { _segmentSpan, ...cleanSpanJSON } = spanJSON; + const { _segmentSpan, _segmentNameAtCapture, ...cleanSpanJSON } = spanJSON; + refreshSegmentName(cleanSpanJSON, _segmentNameAtCapture, () => getCurrentSegmentName(_segmentSpan)); return cleanSpanJSON; }); @@ -193,3 +205,24 @@ export class SpanBuffer { this._traceBuckets.delete(traceId); } } + +/** + * Spans are captured when they end, but the segment span can still be renamed afterwards + * (e.g. once the route is known). Only replace the value we set at capture time, so that + * values set by users or `beforeSendSpan` are kept. + */ +function refreshSegmentName( + spanJSON: SerializedStreamedSpan, + segmentNameAtCapture: string | undefined, + getCurrentSegmentName: () => string, +): void { + const segmentNameAttribute = spanJSON.attributes[SENTRY_SEGMENT_NAME]; + if (segmentNameAtCapture === undefined || segmentNameAttribute?.value !== segmentNameAtCapture) { + return; + } + + const currentSegmentName = getCurrentSegmentName(); + if (currentSegmentName !== segmentNameAtCapture) { + spanJSON.attributes[SENTRY_SEGMENT_NAME] = { ...segmentNameAttribute, value: currentSegmentName }; + } +} diff --git a/packages/core/src/tracing/spans/spanNames.ts b/packages/core/src/tracing/spans/spanNames.ts index da9c2d581854..4815d4ded60c 100644 --- a/packages/core/src/tracing/spans/spanNames.ts +++ b/packages/core/src/tracing/spans/spanNames.ts @@ -20,194 +20,194 @@ import { /** * Fallback name for pageload spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#browser-pageload + * @see https://conventions.sentry.dev/names/#browser-pageload */ export const PAGELOAD_SPAN_NAME_FALLBACK = 'Pageload'; /** * Fallback name for navigation spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#browser-navigation + * @see https://conventions.sentry.dev/names/#browser-navigation */ export const NAVIGATION_SPAN_NAME_FALLBACK = 'Navigation'; /** * Fallback name for db spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#db-queries + * @see https://conventions.sentry.dev/names/#db-queries */ export const DB_SPAN_NAME_FALLBACK = 'Database operation'; /** * Fallback name for gen_ai agent spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#gen_ai-agent + * @see https://conventions.sentry.dev/names/#gen_ai-agent */ export const GEN_AI_AGENT_SPAN_NAME_FALLBACK = 'Generative AI agent operation'; /** * Fallback name for gen_ai model spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#gen_ai-inference + * @see https://conventions.sentry.dev/names/#gen_ai-inference */ export const GEN_AI_INFERENCE_SPAN_NAME_FALLBACK = 'Generative AI model operation'; /** * Fallback name for graphql spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#graphql-graphql + * @see https://conventions.sentry.dev/names/#graphql-graphql */ export const GRAPHQL_SPAN_NAME_FALLBACK = 'GraphQL Operation'; /** * Fallback name for http.(client|server) spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#http + * @see https://conventions.sentry.dev/names/#http */ export const HTTP_SPAN_NAME_FALLBACK = 'HTTP'; /** * Fallback name for messaging spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#messaging + * @see https://conventions.sentry.dev/names/#messaging */ export const MESSAGING_SPAN_NAME_FALLBACK = 'Messaging'; /** * Fallback name for mcp server spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#mcp-server + * @see https://conventions.sentry.dev/names/#mcp-server */ export const MCP_SERVER_SPAN_NAME_FALLBACK = 'MCP server operation'; /** * Fallback name for mcp notification spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#mcp-notification + * @see https://conventions.sentry.dev/names/#mcp-notification */ export const MCP_NOTIFICATION_SPAN_NAME_FALLBACK = 'MCP notification'; /** * Fallback name for resource spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#resource-resources + * @see https://conventions.sentry.dev/names/#resource-resources */ export const RESOURCE_SPAN_NAME_FALLBACK = 'Resource'; /** * Fallback name for router spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#routing-router + * @see https://conventions.sentry.dev/names/#routing-router */ export const ROUTER_SPAN_NAME_FALLBACK = 'Router'; /** * Fallback name for request handler spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#web_server-request-handler + * @see https://conventions.sentry.dev/names/#web_server-request-handler */ export const REQUEST_HANDLER_SPAN_NAME_FALLBACK = 'Request handler'; /** * Fallback name for serverless function execution spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#faas-serverless-function-execution + * @see https://conventions.sentry.dev/names/#faas-serverless-function-execution */ export const SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK = 'Serverless function execution'; /** * Fallback name for function execution spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#function + * @see https://conventions.sentry.dev/names/#function */ export const FUNCTION_SPAN_NAME_FALLBACK = 'Function execution'; /** * Fallback name for ui.mount spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-component-mount + * @see https://conventions.sentry.dev/names/#ui-component-mount */ export const UI_MOUNT_SPAN_NAME_FALLBACK = 'Component mount'; /** * Fallback name for ui.update spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-component-update + * @see https://conventions.sentry.dev/names/#ui-component-update */ export const UI_UPDATE_SPAN_NAME_FALLBACK = 'Component update'; /** * Fallback name for ui.render spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-component-render + * @see https://conventions.sentry.dev/names/#ui-component-render */ export const UI_RENDER_SPAN_NAME_FALLBACK = 'Component render'; /** * Fallback name for ui.unmount spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-component-unmount + * @see https://conventions.sentry.dev/names/#ui-component-unmount */ export const UI_UNMOUNT_SPAN_NAME_FALLBACK = 'Component unmount'; /** * Fallback name for ui.resolve spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-component-resolve + * @see https://conventions.sentry.dev/names/#ui-component-resolve */ export const UI_RESOLVE_SPAN_NAME_FALLBACK = 'Component resolve'; /** * Fallback name for ui.task spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-ui-task + * @see https://conventions.sentry.dev/names/#ui-ui-task */ export const UI_TASK_SPAN_NAME_FALLBACK = 'UI task'; /** * Fallback name for ui.long_task and ui.long_animation_frame spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-main-ui-thread-blocked + * @see https://conventions.sentry.dev/names/#ui-main-ui-thread-blocked */ export const UI_LONG_TASK_SPAN_NAME_FALLBACK = 'Main UI thread blocked'; /** * Fallback name for ui.action spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-ui-action + * @see https://conventions.sentry.dev/names/#ui-ui-action */ export const UI_ACTION_SPAN_NAME_FALLBACK = 'UI action'; /** * Fallback name for ui.action.click spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-click-action + * @see https://conventions.sentry.dev/names/#ui-click-action */ export const UI_ACTION_CLICK_SPAN_NAME_FALLBACK = 'Click'; /** * Fallback name for ui.interaction.click spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-click-interaction + * @see https://conventions.sentry.dev/names/#ui-click-interaction */ export const UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK = 'Click'; /** * Fallback name for ui.interaction.hover spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-hover-interaction + * @see https://conventions.sentry.dev/names/#ui-hover-interaction */ export const UI_INTERACTION_HOVER_SPAN_NAME_FALLBACK = 'Hover'; /** * Fallback name for ui.interaction.drag spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-drag-interaction + * @see https://conventions.sentry.dev/names/#ui-drag-interaction */ export const UI_INTERACTION_DRAG_SPAN_NAME_FALLBACK = 'Drag'; /** * Fallback name for ui.interaction.press spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-key-press-interaction + * @see https://conventions.sentry.dev/names/#ui-key-press-interaction */ export const UI_INTERACTION_PRESS_SPAN_NAME_FALLBACK = 'Key press'; /** * Fallback name for ui.webvital.lcp spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-largest-contentful-paint + * @see https://conventions.sentry.dev/names/#ui-largest-contentful-paint */ export const UI_WEBVITAL_LCP_SPAN_NAME_FALLBACK = 'Largest contentful paint'; /** * Fallback name for ui.webvital.cls spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-layout-shift + * @see https://conventions.sentry.dev/names/#ui-layout-shift */ export const UI_WEBVITAL_CLS_SPAN_NAME_FALLBACK = 'Layout shift'; /** * Fallback name for generic ui spans when no better-suited span name is available. - * @see https://getsentry.github.io/sentry-conventions/names/#ui-ui + * @see https://conventions.sentry.dev/names/#ui-ui */ export const UI_SPAN_NAME_FALLBACK = 'UI'; /** * The `cache.operation` attribute value each cache op carries. Cache span names are * `cache.{{cache.operation}}`, so the op constant itself doubles as the low-cardinality span name. - * @see https://getsentry.github.io/sentry-conventions/names/#cache + * @see https://conventions.sentry.dev/names/#cache */ export const CACHE_OPERATION_NAMES = { [CACHE_GET]: 'get', @@ -218,7 +218,7 @@ export const CACHE_OPERATION_NAMES = { /** * Span names for the browser navigation timing ops, keyed by op. None of these ops has an attribute * template, so the static name is the only name they can get. - * @see https://getsentry.github.io/sentry-conventions/names/#browser-navigation-timing + * @see https://conventions.sentry.dev/names/#browser-navigation-timing */ export const BROWSER_NAVIGATION_TIMING_SPAN_NAMES = { [BROWSER_CACHE]: 'Cache lookup', diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 443a9f1e59d5..371301903707 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -671,6 +671,15 @@ export interface BrowserTunnelOptions extends TunnelOptions { export interface ServerTunnelOptions extends TunnelOptions { /** DSNs to forward besides the server's own, e.g. a different browser DSN. */ allowedDsns?: string[]; + + /** + * Whether the SDK answers tunnel requests itself, before they reach the app. Set it to `false` to serve the tunnel + * from your own code instead, e.g. behind your own middleware, with `handleTunnelRequestIfMatched`. Tunnel requests + * still get no session or span then, but errors your code throws while answering them are captured. + * + * @default true + */ + intercept?: boolean; } /** diff --git a/packages/core/src/utils/node-stack-trace.ts b/packages/core/src/utils/node-stack-trace.ts index 3861a84bdeba..9dcfd03010bc 100644 --- a/packages/core/src/utils/node-stack-trace.ts +++ b/packages/core/src/utils/node-stack-trace.ts @@ -52,7 +52,7 @@ export function filenameIsInApp(filename: string, isNative: boolean = false): bo /** Node Stack line parser */ export function node(getModule?: GetModuleFn): StackLineParserFn { const FILENAME_MATCH = /^\s*[-]{4,}$/; - const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/; + const FULL_MATCH = /at (?:async )?(?:(?=.*\)\s*$)(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/; const DATA_URI_MATCH = /at (?:async )?(.+?) \(data:(.*?),/; return (line: string) => { diff --git a/packages/core/src/utils/normalize.ts b/packages/core/src/utils/normalize.ts index 0a31a0c53885..059f69b44892 100644 --- a/packages/core/src/utils/normalize.ts +++ b/packages/core/src/utils/normalize.ts @@ -57,7 +57,21 @@ export function normalize(input: unknown, depth: number = 100, maxProperties: nu // since we're at the outermost level, we don't provide a key return visit('', input, depth, maxProperties); } catch (err) { - return { ERROR: `**non-serializable** (${err})` }; + return { ERROR: `**non-serializable** (${stringifyCaughtValue(err)})` }; + } +} + +/** + * Stringify a caught value for an error message. Template literals and other implicit + * conversions throw on symbols and on objects with no usable `toString`, which is how + * the values that end up here tend to be broken in the first place, so fall back to + * fixed text instead of throwing again. + */ +function stringifyCaughtValue(err: unknown): string { + try { + return String(err); + } catch { + return 'unknown error'; } } @@ -240,7 +254,7 @@ export function stringifyValue( return `[object ${objName}]`; } catch (err) { - return `**non-serializable** (${err})`; + return `**non-serializable** (${stringifyCaughtValue(err)})`; } } /* eslint-enable complexity */ diff --git a/packages/core/src/utils/object.ts b/packages/core/src/utils/object.ts index 46f40c2cd3c7..51be633812c8 100644 --- a/packages/core/src/utils/object.ts +++ b/packages/core/src/utils/object.ts @@ -18,7 +18,11 @@ import { isError, isEvent, isObjectLike, isPrimitive } from './is'; * @returns void */ -export function fill(source: { [key: string]: any }, name: string, replacementFactory: (...args: any[]) => any): void { +export function fill( + source: { [key: string | symbol]: any }, + name: string | symbol, + replacementFactory: (...args: any[]) => any, +): void { if (!(name in source)) { return; } @@ -41,7 +45,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa try { source[name] = wrapped; } catch { - DEBUG_BUILD && debug.log(`Failed to replace method "${name}" in object`, source); + DEBUG_BUILD && debug.log(`Failed to replace method "${String(name)}" in object`, source); } } diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts index 7c92511185ab..82c7135fb2ac 100644 --- a/packages/core/src/utils/request.ts +++ b/packages/core/src/utils/request.ts @@ -274,8 +274,8 @@ function getAbsoluteUrl({ * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/http/#http-request-header * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/http/#http-response-header * - * @see https://getsentry.github.io/sentry-conventions/attributes/http/#http-request-header-key - * @see https://getsentry.github.io/sentry-conventions/attributes/http/#http-response-header-key + * @see https://conventions.sentry.dev/attributes/http/#http-request-header-key + * @see https://conventions.sentry.dev/attributes/http/#http-response-header-key */ export function httpHeadersToSpanAttributes( headers: Record, diff --git a/packages/core/src/utils/tunnel.ts b/packages/core/src/utils/tunnel.ts index 62144a180134..1419dce0f648 100644 --- a/packages/core/src/utils/tunnel.ts +++ b/packages/core/src/utils/tunnel.ts @@ -1,11 +1,13 @@ import type { Client } from '../client'; -import type { Integration } from '../types/integration'; import { getClient } from '../currentScopes'; import { URL_PATH } from '@sentry/conventions/attributes'; import type { DsnComponents, DsnLike } from '../types/dsn'; import type { IgnoreSpanFilter, ServerTunnelOptions, TunnelInit, TunnelOptions } from '../types/options'; +import { DEBUG_BUILD } from '../debug-build'; import { escapeStringForRegex } from '../vendor/escapeStringForRegex'; +import { debug } from './debug-logger'; import { dsnToString, makeDsn } from './dsn'; +import { getClientTunnel, setClientTunnel } from './tunnelStore'; import { parseUrl } from './url'; /** @@ -16,18 +18,12 @@ export type TunnelForwarder = (request: Request, allowedDsns: Array) => /** * A server client's tunnel paths, base path, allowed DSNs and forwarder, computed once per client. */ -interface ServedTunnel extends TunnelMatch { +export interface ServedTunnel extends TunnelMatch { forward: TunnelForwarder; + /** Whether the SDK answers tunnel requests itself, see `ServerTunnelOptions.intercept`. */ + intercept: boolean; } -const servedTunnels = new WeakMap(); - -/** - * The name of `serveTunnelIntegration`, which carries the forwarder. Kept here so this module doesn't import the - * integration, and with it the forwarder, into apps that don't use it. - */ -export const SERVE_TUNNEL_INTEGRATION_NAME = 'ServeTunnel'; - /** * Computes a server client's served tunnel from its resolved options, and stores it. */ @@ -42,40 +38,19 @@ export function setServedTunnel( basePath: tunnelOptions.basePath, allowedDsns, forward, + intercept: tunnelOptions.intercept !== false, }; - servedTunnels.set(client, servedTunnel); + setClientTunnel(client, { served: servedTunnel }); return servedTunnel; } /** - * The forwarder `serveTunnelIntegration` carries. Any copy of this module can reach it through the client. - */ -export function getServeTunnelForwarder(client: Client): TunnelForwarder | undefined { - return client.getIntegrationByName?.(SERVE_TUNNEL_INTEGRATION_NAME) - ?.forward; -} - -/** - * The active client's served tunnel. A copy of this module that didn't set it up rebuilds it once from what the - * client carries. Bundlers can give a framework's request hook its own copy, like Next.js's middleware next to its - * `instrumentation`. + * The active client's served tunnel. It lives in the tunnel store, so a copy of this module that didn't set it up, + * like the one a bundler gives Next.js's middleware next to its `instrumentation`, still finds it. */ -function getServedTunnel(): ServedTunnel | undefined { - const client = getClient(); - if (!client) { - return undefined; - } - - const servedTunnel = servedTunnels.get(client); - if (servedTunnel) { - return servedTunnel; - } - - const tunnelOptions = client.getOptions()._managedTunnel; - const forward = getServeTunnelForwarder(client); - - return tunnelOptions && forward ? setServedTunnel(client, tunnelOptions, forward) : undefined; +export function getServedTunnel(client = getClient()): ServedTunnel | undefined { + return client && getClientTunnel(client)?.served; } /** @@ -83,7 +58,7 @@ function getServedTunnel(): ServedTunnel | undefined { * A path the user wrote, as a string or `path`, is used as written, so a tunnel served outside the app keeps working. * The base path is normalized, and a root one is no default at all. */ -export function applyTunnelDefaults( +export function applyBrowserTunnelDefaults( tunnel: TunnelInit | undefined, defaults: TunnelOptions | undefined, ): TunnelInit | undefined { @@ -219,26 +194,71 @@ export function isTunnelRequest(request: TunnelRequestLike, options?: ServerTunn } /** - * Returns true if the request targets the tunnel `serveTunnelIntegration` serves. Unlike {@link isTunnelRequest}, - * there's no fallback, so the framework SDKs serve nothing without the integration. + * Returns true if the request targets the tunnel `serveTunnelIntegration` serves, whether the SDK answers it or the + * app does with `intercept: false`. The server SDKs record no session or span for these. Unlike + * {@link isTunnelRequest}, there's no fallback, so the framework SDKs serve nothing without the integration. `client` + * defaults to the active one. */ -export function isServedTunnelRequest(request: TunnelRequestLike): boolean { - const servedTunnel = getServedTunnel(); +export function isServedTunnelRequest(request: TunnelRequestLike, client?: Client): boolean { + try { + return !!matchServedTunnel(request, client); + } catch (error) { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to match a request', error); - return !!servedTunnel && matchesTunnel(request, servedTunnel); + return false; + } +} + +/** + * The served tunnel, if the SDK answers this request on it. `client` defaults to the active one. + */ +export function getServedTunnelMatch(request: TunnelRequestLike, client?: Client): ServedTunnel | undefined { + const servedTunnel = matchServedTunnel(request, client); + + return servedTunnel?.intercept ? servedTunnel : undefined; +} + +/** + * The served tunnel, if this request targets it. + */ +function matchServedTunnel(request: TunnelRequestLike, client?: Client): ServedTunnel | undefined { + const servedTunnel = getServedTunnel(client); + + return servedTunnel?.allowedDsns.length && matchesTunnel(request, servedTunnel) ? servedTunnel : undefined; } /** * Forwards the request if it targets the tunnel `serveTunnelIntegration` serves, otherwise returns `undefined`. The - * framework SDKs answer tunnel requests with it. + * runtime and framework SDKs answer tunnel requests with it, ahead of the app. + * + * It never throws, since a throw here would break the app's own requests. Until a request is claimed, a failure hands + * it to the app. Once claimed, its body may be read already, so a failure ends in a 502 instead. `client` defaults to + * the active one. */ -export function handleServedTunnelRequest(request: Request): Promise | undefined { - const servedTunnel = getServedTunnel(); - if (!servedTunnel?.allowedDsns.length || !matchesTunnel(request, servedTunnel)) { +export function handleServedTunnelRequest(request: Request, client?: Client): Promise | undefined { + let servedTunnel: ServedTunnel | undefined | null; + try { + // An envelope always has a body, so a request without one is never a tunnel request. + servedTunnel = request.body && getServedTunnelMatch(request, client); + if (!servedTunnel) { + return undefined; + } + } catch (error) { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to match a request', error); + return undefined; } - return servedTunnel.forward(request, servedTunnel.allowedDsns); + // Claimed from here on, so even a `forward` that throws right away ends in a 502. + const { forward, allowedDsns } = servedTunnel; + + return Promise.resolve() + .then(() => forward(request, allowedDsns)) + .catch(error => { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to forward a request', error); + + return new Response(null, { status: 502 }); + }); } /** diff --git a/packages/core/src/utils/managedTunnelDefaults.ts b/packages/core/src/utils/tunnelDefaults.ts similarity index 59% rename from packages/core/src/utils/managedTunnelDefaults.ts rename to packages/core/src/utils/tunnelDefaults.ts index c78cdd55136d..7583749cd8a2 100644 --- a/packages/core/src/utils/managedTunnelDefaults.ts +++ b/packages/core/src/utils/tunnelDefaults.ts @@ -1,18 +1,14 @@ import type { Client } from '../client'; import { getClient } from '../currentScopes'; -import type { IgnoreSpanFilter, ServerTunnelOptions } from '../types/options'; +import type { ServerTunnelOptions } from '../types/options'; import type { TunnelForwarder } from './tunnel'; -import { - getServeTunnelForwarder, - getTunnelIgnoreSpanFilters, - normalizeTunnelBasePath, - setServedTunnel, -} from './tunnel'; +import { getClientTunnel, getStoredTunnelDefaults, setClientTunnel, setStoredTunnelDefaults } from './tunnelStore'; +import { getTunnelIgnoreSpanFilters, normalizeTunnelBasePath, setServedTunnel } from './tunnel'; /** * Managed tunnel settings a framework SDK knows outside `init`. */ -interface ManagedTunnelDefaults { +export interface TunnelDefaults { /** * Enables the managed tunnel when `init` didn't, e.g. from TanStack Start's `tunnelRoute` build option. Its base * path is `basePath`, so it has none of its own. @@ -27,24 +23,17 @@ interface ManagedTunnelDefaults { basePath?: string; } -let managedTunnelDefaults: ManagedTunnelDefaults | undefined; - -const tunnelForwarders = new WeakMap(); - -// So re-applying defaults replaces a client's filters instead of adding more. -const installedSpanFilters = new WeakMap(); - /** * Sets framework-known tunnel settings for the current and every later server client, so per-request clients * (Cloudflare) start with them. Settings from `init` win. Framework SDKs call this once at load. */ -export function setManagedTunnelDefaults(defaults: ManagedTunnelDefaults): void { - managedTunnelDefaults = { - ...managedTunnelDefaults, +export function setTunnelDefaults(defaults: TunnelDefaults): void { + setStoredTunnelDefaults({ + ...getStoredTunnelDefaults(), ...(defaults.tunnel && { tunnel: defaults.tunnel, forward: defaults.forward }), ...(defaults.basePath !== undefined && { basePath: defaults.basePath }), - }; - applyManagedTunnelDefaults(getClient()); + }); + applyServerTunnelDefaults(getClient()); } /** @@ -56,26 +45,25 @@ export function enableManagedTunnel( forward: TunnelForwarder, ): void { client.getOptions()._managedTunnel = { ...tunnelOptions }; - tunnelForwarders.set(client, forward); - applyManagedTunnelDefaults(client); + setClientTunnel(client, { forward }); + applyServerTunnelDefaults(client); } /** * Resolves a server client's managed tunnel (from `serveTunnelIntegration` or the defaults), computes its paths and * allowed DSNs, and installs its span filters. Runs when a server client is created and from - * {@link setManagedTunnelDefaults}. + * {@link setTunnelDefaults}. */ -export function applyManagedTunnelDefaults(client: Client | undefined): void { +export function applyServerTunnelDefaults(client: Client | undefined): void { + const tunnelDefaults = getStoredTunnelDefaults(); const options = client?.getOptions(); - const tunnelOptions: ServerTunnelOptions | undefined = options?._managedTunnel ?? managedTunnelDefaults?.tunnel; - // A framework SDK can bundle its own copy of this module, which only reaches the forwarder through the client. - const forward = - client && (tunnelForwarders.get(client) ?? getServeTunnelForwarder(client) ?? managedTunnelDefaults?.forward); + const tunnelOptions: ServerTunnelOptions | undefined = options?._managedTunnel ?? tunnelDefaults?.tunnel; + const forward = client && (getClientTunnel(client)?.forward ?? tunnelDefaults?.forward); if (!client || !options || !tunnelOptions || !forward) { return; } - const basePath = normalizeTunnelBasePath(managedTunnelDefaults?.basePath); + const basePath = normalizeTunnelBasePath(tunnelDefaults?.basePath); const resolved = tunnelOptions.basePath === undefined && !tunnelOptions.path && basePath ? { ...tunnelOptions, basePath } @@ -83,20 +71,20 @@ export function applyManagedTunnelDefaults(client: Client | undefined): void { options._managedTunnel = resolved; const { paths } = setServedTunnel(client, resolved, forward); - const previousFilters = installedSpanFilters.get(client) ?? []; + const previousFilters = getClientTunnel(client)?.spanFilters ?? []; const spanFilters = getTunnelIgnoreSpanFilters(resolved, paths); options.ignoreSpans = [ ...(options.ignoreSpans ?? []).filter(filter => !(previousFilters as unknown[]).includes(filter)), ...spanFilters, ]; - installedSpanFilters.set(client, spanFilters); + setClientTunnel(client, { spanFilters }); } /** - * Resets {@link setManagedTunnelDefaults}, for tests. + * Resets {@link setTunnelDefaults}, for tests. * * @internal */ -export function _INTERNAL_resetManagedTunnelDefaults(): void { - managedTunnelDefaults = undefined; +export function _INTERNAL_resetTunnelDefaults(): void { + setStoredTunnelDefaults(undefined); } diff --git a/packages/core/src/utils/tunnelForward.ts b/packages/core/src/utils/tunnelForward.ts index 01f2af0947bd..9e3298d7e9f9 100644 --- a/packages/core/src/utils/tunnelForward.ts +++ b/packages/core/src/utils/tunnelForward.ts @@ -1,10 +1,12 @@ +/* oxlint-disable max-lines */ import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api'; import { getClient } from '../currentScopes'; import { suppressTracing } from '../tracing/trace'; import { debug } from './debug-logger'; import { makeDsn } from './dsn'; import type { ServerTunnelOptions } from '../types/options'; -import { matchesTunnel, resolveAllowedDsns, resolveTunnelMatch } from './tunnel'; +import { DEBUG_BUILD } from '../debug-build'; +import { getServedTunnelMatch, matchesTunnel, resolveAllowedDsns, resolveTunnelMatch } from './tunnel'; /** * Options for {@link handleTunnelRequest}. @@ -43,6 +45,7 @@ interface NodeTunnelRequest extends AsyncIterable { interface NodeTunnelResponse { writeHead(statusCode: number, headers?: Record): unknown; end(chunk: Uint8Array): unknown; + headersSent?: boolean; } /** @@ -127,6 +130,53 @@ export function handleTunnelRequestIfMatched( ); } +/** + * Answers a tunnel request on a Node server ahead of the app, see {@link handleServedNodeTunnelRequest}. + */ +export type NodeTunnelHandler = (request: NodeTunnelRequest, response: NodeTunnelResponse) => boolean; + +/** + * Answers a request to the tunnel `serveTunnelIntegration` serves on a Node server, ahead of the app. Returns `true` + * when it claimed the request and answers it, `false` to hand the request to the app. + * + * It never throws, since it runs inside the server's `emit`, where a throw crashes the process. Until a request is + * claimed, a failure hands it to the app. Once claimed, its body may be read already, so a failure ends in a 502. + */ +export function handleServedNodeTunnelRequest(request: NodeTunnelRequest, response: NodeTunnelResponse): boolean { + let forward: () => Promise; + try { + const servedTunnel = getServedTunnelMatch(request); + if (!servedTunnel) { + return false; + } + + const fetchRequest = toFetchRequest(request); + forward = () => servedTunnel.forward(fetchRequest, servedTunnel.allowedDsns); + } catch (error) { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to match a request', error); + + return false; + } + + // Claimed from here on, so even a `forward` that throws right away ends in a 502. + Promise.resolve() + .then(forward) + .then(fetchResponse => writeNodeResponse(response, fetchResponse)) + .catch(error => { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to forward a request', error); + try { + if (!response.headersSent) { + response.writeHead(502); + } + response.end(new Uint8Array()); + } catch { + // The connection is gone. + } + }); + + return true; +} + /** * `allowedDsns` plus the server's own DSN. Without `allowedDsns`, the DSNs `serveTunnelIntegration` allows, so its * options apply here too. @@ -143,6 +193,21 @@ function getAllowedDsns(options: TunnelForwardOptions | undefined): string[] { */ const FORWARDED_RESPONSE_HEADERS = ['content-type', 'retry-after', 'x-sentry-rate-limits']; +/** + * The headers of Sentry's answer to pass on to the browser, see `FORWARDED_RESPONSE_HEADERS`. + */ +export function pickForwardedHeaders(response: Response): Record { + const headers: Record = {}; + for (const name of FORWARDED_RESPONSE_HEADERS) { + const value = response.headers.get(name); + if (value) { + headers[name] = value; + } + } + + return headers; +} + function isNodeTunnelResponse(value: unknown): value is NodeTunnelResponse { return typeof (value as NodeTunnelResponse | undefined)?.writeHead === 'function'; } @@ -155,12 +220,14 @@ function toFetchRequest(request: NodeTunnelRequest): Request { headers.set(name, Array.isArray(value) ? value.join(', ') : value); } } - const iterator = request[Symbol.asyncIterator](); + // Created on the first read, so building the `Request` doesn't touch the body. + let iterator: AsyncIterator | undefined; + const getIterator = (): AsyncIterator => (iterator ??= request[Symbol.asyncIterator]()); const encoder = new TextEncoder(); // Not `ReadableStream.from`, which Bun 1.3 lacks. const body = new ReadableStream({ async pull(controller) { - const { done, value } = await iterator.next(); + const { done, value } = await getIterator().next(); if (done) { controller.close(); return; @@ -170,7 +237,7 @@ function toFetchRequest(request: NodeTunnelRequest): Request { }, // Node only reads the rest of a body nobody started reading, and unread bytes stall a keep-alive connection. async cancel() { - while (!(await iterator.next()).done) { + while (!(await getIterator().next()).done) { // Drain. } }, @@ -195,14 +262,7 @@ async function writeNodeResponse(response: NodeTunnelResponse, fetchResponse: Re return; } - const responseHeaders: Record = {}; - for (const name of FORWARDED_RESPONSE_HEADERS) { - const value = fetchResponse.headers.get(name); - if (value) { - responseHeaders[name] = value; - } - } - response.writeHead(fetchResponse.status, responseHeaders); + response.writeHead(fetchResponse.status, pickForwardedHeaders(fetchResponse)); response.end(new Uint8Array(body)); } @@ -222,6 +282,11 @@ export async function forwardTunnelRequest(request: Request, allowedDsns: Array< return withDecodedBodyHeaders(await forwardEnvelope(request, allowedDsns)); } +/** + * How long a forward waits on Sentry, so a hanging Sentry doesn't hold the browser's connection open. + */ +const FORWARD_TIMEOUT_MS = 30_000; + async function forwardEnvelope(request: Request, allowedDsns: Array): Promise { if (allowedDsns.length === 0) { return new Response('Tunnel not configured', { status: 500 }); @@ -254,6 +319,9 @@ async function forwardEnvelope(request: Request, allowedDsns: Array): Pr headers: forwardHeaders(request), body: prependToStream(head.read, reader), duplex: 'half', + // Not the browser's signal: once the envelope is here, it still goes to Sentry after the browser leaves. A + // browser that leaves mid-upload breaks the body, which fails the forward on its own. + signal: AbortSignal.timeout(FORWARD_TIMEOUT_MS), } as RequestInit), ); } catch (error) { diff --git a/packages/core/src/utils/tunnelStore.ts b/packages/core/src/utils/tunnelStore.ts new file mode 100644 index 000000000000..a6dc118087f9 --- /dev/null +++ b/packages/core/src/utils/tunnelStore.ts @@ -0,0 +1,96 @@ +import type { Client } from '../client'; +import { getGlobalSingleton } from '../carrier'; +import type { IgnoreSpanFilter } from '../types/options'; +import type { TunnelDefaults } from './tunnelDefaults'; +import type { ServedTunnel, TunnelForwarder } from './tunnel'; +import type { NodeTunnelHandler } from './tunnelForward'; + +/** + * What the tunnel keeps per server client. + */ +export interface ClientTunnelState { + /** The paths, allowed DSNs and forwarder the client answers tunnel requests with. */ + served?: ServedTunnel; + /** The forwarder `serveTunnelIntegration` enabled the tunnel with. */ + forward?: TunnelForwarder; + /** The span filters the tunnel installed, so applying it again replaces them instead of adding more. */ + spanFilters?: IgnoreSpanFilter[]; +} + +/** + * The tunnel state, shared by every copy of `@sentry/core`: a framework's server bundle can carry its own + * copy, which sets the tunnel up for the runtime's copy that answers it. + */ +export interface TunnelStore { + clients: WeakMap; + defaults?: TunnelDefaults; + /** Whether core's HTTP server instrumentation is set up, whose `emit` wrapper handles tunnel requests itself. */ + httpServerHandlesTunnel?: boolean; + /** Answers tunnel requests on Node servers. The tunnel sets it, so apps without one don't bundle it. */ + nodeHandler?: NodeTunnelHandler; +} + +/** + * Only this module touches the store, through the functions below. + */ +function getTunnelStore(): TunnelStore { + return getGlobalSingleton('tunnelStore', () => ({ clients: new WeakMap() })); +} + +/** + * What the tunnel keeps for a client. + */ +export function getClientTunnel(client: Client): Readonly | undefined { + return getTunnelStore().clients.get(client); +} + +/** + * Sets what the tunnel keeps for a client. Merges into what is already there, like `scope.setTags`. + */ +export function setClientTunnel(client: Client, state: ClientTunnelState): void { + const { clients } = getTunnelStore(); + clients.set(client, { ...clients.get(client), ...state }); +} + +/** + * The framework-known tunnel settings, see `setTunnelDefaults`. + */ +export function getStoredTunnelDefaults(): Readonly | undefined { + return getTunnelStore().defaults; +} + +/** + * Replaces the framework-known tunnel settings. + */ +export function setStoredTunnelDefaults(defaults: TunnelDefaults | undefined): void { + getTunnelStore().defaults = defaults; +} + +/** + * Records that core's HTTP server instrumentation is set up. Its `emit` wrapper handles tunnel requests, so `node:http` + * servers need no second one for the tunnel. + */ +export function setHttpServerHandlesTunnel(): void { + getTunnelStore().httpServerHandlesTunnel = true; +} + +/** + * Whether core's HTTP server instrumentation handles tunnel requests, see {@link setHttpServerHandlesTunnel}. + */ +export function httpServerHandlesTunnel(): boolean { + return !!getTunnelStore().httpServerHandlesTunnel; +} + +/** + * Sets what answers tunnel requests on Node servers, like the forwarder `serveTunnelIntegration` brings in. + */ +export function setNodeTunnelHandler(handler: NodeTunnelHandler): void { + getTunnelStore().nodeHandler = handler; +} + +/** + * What answers tunnel requests on Node servers, see {@link setNodeTunnelHandler}. + */ +export function getNodeTunnelHandler(): NodeTunnelHandler | undefined { + return getTunnelStore().nodeHandler; +} diff --git a/packages/core/src/utils/weightBasedFlushing.ts b/packages/core/src/utils/weightBasedFlushing.ts new file mode 100644 index 000000000000..7a50bd83fd0a --- /dev/null +++ b/packages/core/src/utils/weightBasedFlushing.ts @@ -0,0 +1,149 @@ +import type { Client } from '../client'; +import type { Log } from '../types/log'; +import type { Metric } from '../types/metric'; +import type { Primitive } from '../types/misc'; +import { isPrimitive } from './is'; +import { safeUnref } from './timer'; + +// Default interval for flushing logs and metrics (5 seconds) +const DEFAULT_FLUSH_INTERVAL = 5000; + +/** + * Sets up weight-based flushing for logs or metrics. + * This helper function encapsulates the common pattern of: + * 1. Tracking accumulated weight of items + * 2. Flushing when weight exceeds threshold (800KB) + * 3. Flushing after timeout period from the first item + * + * Uses closure variables to track weight and timeout state. + */ +export function setupWeightBasedFlushing< + T, + AfterCaptureHook extends 'afterCaptureLog' | 'afterCaptureMetric', + FlushHook extends 'flushLogs' | 'flushMetrics', +>( + client: Client, + afterCaptureHook: AfterCaptureHook, + flushHook: FlushHook, + estimateSizeFn: (item: T) => number, + flushFn: (client: Client) => void, +): void { + // Track weight and timeout in closure variables + let weight = 0; + let flushTimeout: ReturnType | undefined; + let isTimerActive = false; + + // @ts-expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe + client.on(flushHook, () => { + weight = 0; + clearTimeout(flushTimeout); + isTimerActive = false; + }); + + // @ts-expect-error - TypeScript can't narrow generic hook types to match specific overloads, but we know this is type-safe + client.on(afterCaptureHook, (item: T) => { + weight += estimateSizeFn(item); + + // We flush the buffer if it exceeds 0.8 MB + // The weight is a rough estimate, so we flush way before the payload gets too big. + if (weight >= 800_000) { + flushFn(client); + } else if (!isTimerActive) { + const flushInterval = client.getOptions()._flushInterval ?? DEFAULT_FLUSH_INTERVAL; + if (flushInterval > 0) { + // Only start timer if one isn't already running. + // This prevents flushing being delayed by items that arrive close to the timeout limit + // and thus resetting the flushing timeout and delaying items being flushed. + isTimerActive = true; + // Use safeUnref so the timer doesn't prevent the process from exiting + flushTimeout = safeUnref( + setTimeout(() => { + flushFn(client); + // Note: isTimerActive is reset by the flushHook handler above, not here, + // to avoid race conditions when new items arrive during the flush. + }, flushInterval), + ); + } + } + }); + + client.on('flush', () => { + flushFn(client); + }); +} + +/** + * Estimate the size of a metric in bytes. + * + * @param metric - The metric to estimate the size of. + * @returns The estimated size of the metric in bytes. + */ +export function estimateMetricSizeInBytes(metric: Metric): number { + let weight = 0; + + // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. + if (metric.name) { + weight += metric.name.length * 2; + } + + // Add weight for number + weight += 8; + + return weight + estimateAttributesSizeInBytes(metric.attributes); +} + +/** + * Estimate the size of a log in bytes. + * + * @param log - The log to estimate the size of. + * @returns The estimated size of the log in bytes. + */ +export function estimateLogSizeInBytes(log: Log): number { + let weight = 0; + + // Estimate byte size of 2 bytes per character. This is a rough estimate JS strings are stored as UTF-16. + if (log.message) { + weight += log.message.length * 2; + } + + return weight + estimateAttributesSizeInBytes(log.attributes); +} + +/** + * Estimate the size of attributes in bytes. + * + * @param attributes - The attributes object to estimate the size of. + * @returns The estimated size of the attributes in bytes. + */ +function estimateAttributesSizeInBytes(attributes: Record | undefined): number { + if (!attributes) { + return 0; + } + + let weight = 0; + + Object.values(attributes).forEach(value => { + if (Array.isArray(value)) { + weight += value.length * estimatePrimitiveSizeInBytes(value[0]); + } else if (isPrimitive(value)) { + weight += estimatePrimitiveSizeInBytes(value); + } else { + // For objects values, we estimate the size of the object as 100 bytes + weight += 100; + } + }); + + return weight; +} + +function estimatePrimitiveSizeInBytes(value: Primitive): number { + if (typeof value === 'string') { + return value.length * 2; + } else if (typeof value === 'number') { + return 8; + } else if (typeof value === 'boolean') { + return 4; + } + + return 0; +} diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index 2ca3d60bc21d..fd88b420a2bc 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -95,6 +95,16 @@ export type InternalGlobal = { * Dedupes the one-time warning and short-circuits repeat calls. */ runtimeUnavailable?: boolean; + /** + * Set when an integration asks to subscribe to orchestrion-injected + * channels, so a missing injection is only reported when it loses data. + */ + channelIntegrations?: boolean; + /** + * Set once the Bun warning for missing channel injection has been shown. + * Lives on the marker so that duplicate SDK copies warn only once. + */ + warnedBunRunNotInjected?: boolean; /** * Module namespaces stashed by build-time provider imports, keyed by module * name (e.g. `@mastra/observability`). A bundler plugin can splice a static diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 5d06b33e40d7..8d05b74cf9d8 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -3144,19 +3144,20 @@ describe('Client', () => { test('close flushes the logs buffer', async () => { vi.useRealTimers(); - const flushLogsSpy = vi - .spyOn(logsInternalModule, '_INTERNAL_flushLogsBuffer') - .mockImplementation(() => undefined); - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); - await client.close(); + const flushLogsHandler = vi.fn(); + client.on('flushLogs', flushLogsHandler); + + _INTERNAL_captureLog({ message: 'test log', level: 'info' }, scope); - expect(flushLogsSpy).toHaveBeenCalledTimes(1); - expect(flushLogsSpy).toHaveBeenCalledWith(client); + await client.close(); - flushLogsSpy.mockRestore(); + expect(flushLogsHandler).toHaveBeenCalledTimes(1); + expect(logsInternalModule._INTERNAL_getLogBuffer(client)).toEqual([]); }); test('multiple concurrent flush calls should just work', async () => { @@ -4215,6 +4216,24 @@ describe('Client', () => { vi.useRealTimers(); }); + it('sets up log flushing on the first captured log only', () => { + const onSpy = vi.spyOn(TestClient.prototype, 'on'); + const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + const scope = new Scope(); + scope.setClient(client); + + expect(onSpy).not.toHaveBeenCalledWith('afterCaptureLog', expect.any(Function)); + + _INTERNAL_captureLog({ message: 'test log 1', level: 'info' }, scope); + expect(onSpy).toHaveBeenCalledWith('afterCaptureLog', expect.any(Function)); + + onSpy.mockClear(); + _INTERNAL_captureLog({ message: 'test log 2', level: 'info' }, scope); + expect(onSpy).not.toHaveBeenCalledWith('afterCaptureLog', expect.any(Function)); + + onSpy.mockRestore(); + }); + it('flushes logs when weight exceeds 800KB', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, @@ -4398,6 +4417,24 @@ describe('Client', () => { vi.useRealTimers(); }); + it('sets up metric flushing on the first captured metric only', () => { + const onSpy = vi.spyOn(TestClient.prototype, 'on'); + const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + const scope = new Scope(); + scope.setClient(client); + + expect(onSpy).not.toHaveBeenCalledWith('afterCaptureMetric', expect.any(Function)); + + _INTERNAL_captureMetric({ name: 'metric1', value: 1, type: 'counter', attributes: {} }, { scope }); + expect(onSpy).toHaveBeenCalledWith('afterCaptureMetric', expect.any(Function)); + + onSpy.mockClear(); + _INTERNAL_captureMetric({ name: 'metric2', value: 2, type: 'counter', attributes: {} }, { scope }); + expect(onSpy).not.toHaveBeenCalledWith('afterCaptureMetric', expect.any(Function)); + + onSpy.mockRestore(); + }); + it('flushes metrics when weight exceeds 800KB', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts index e42c83a77611..d47103b9132f 100644 --- a/packages/core/test/lib/integrations/functiontostring.test.ts +++ b/packages/core/test/lib/integrations/functiontostring.test.ts @@ -120,6 +120,51 @@ describe('FunctionToString', () => { expect(Reflect.apply(Function.prototype.toString, inspectedFunction, new Array(100_000))).toBe(expected); }); + it('does not overflow the stack when the prototype chain loops back through a Proxy', () => { + function inspectedFunction(): void {} + const expected = inspectedFunction.toString(); + Object.setPrototypeOf(inspectedFunction, Object.create(new Proxy(inspectedFunction, {}))); + const foo = { + bar(): void {}, + }; + const originalBar = foo.bar.toString(); + fill(foo, 'bar', () => function wrapped(): void {}); + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + + expect(Function.prototype.toString.call(inspectedFunction)).toBe(expected); + expect(foo.bar.toString()).toBe(originalBar); + }); + + it('does not unwrap a function that only inherits the original from a wrapped function', () => { + const foo = { + bar(): void {}, + }; + fill(foo, 'bar', () => function wrapped(): void {}); + function inheriting(): void {} + const expected = inheriting.toString(); + Object.setPrototypeOf(inheriting, foo.bar); + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + + expect(Function.prototype.toString.call(inheriting)).toBe(expected); + }); + + it('falls back to native toString when the original function lookup throws', () => { + function inspectedFunction(): void {} + const throwingTrap = (): never => { + throw new Error('trap'); + }; + const proxy = new Proxy(inspectedFunction, { get: throwingTrap, getOwnPropertyDescriptor: throwingTrap }); + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + + expect(Function.prototype.toString.call(proxy)).toBe('function () { [native code] }'); + }); + it('falls back to native toString and does not throw when the carrier read throws', () => { const foo = { bar(wat: boolean): boolean { diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 23cdaba20cf6..e7f2bc39a90d 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -127,6 +127,7 @@ describe('captureSpan', () => { }, }, _segmentSpan: span, + _segmentNameAtCapture: 'my-span', }); }); @@ -235,6 +236,7 @@ describe('captureSpan', () => { }, }, _segmentSpan: span, + _segmentNameAtCapture: 'my-span', }); }); @@ -307,6 +309,7 @@ describe('captureSpan', () => { }, }, _segmentSpan: span, + _segmentNameAtCapture: 'my-span', }); }); @@ -366,6 +369,7 @@ describe('captureSpan', () => { }, }, _segmentSpan: span, + _segmentNameAtCapture: 'my-span', }); }); diff --git a/packages/core/test/lib/tracing/spans/spanBuffer.test.ts b/packages/core/test/lib/tracing/spans/spanBuffer.test.ts index 224ab4504dd2..281a09485a00 100644 --- a/packages/core/test/lib/tracing/spans/spanBuffer.test.ts +++ b/packages/core/test/lib/tracing/spans/spanBuffer.test.ts @@ -1,9 +1,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Client, StreamedSpanEnvelope } from '../../../../src'; -import { SentrySpan, setCurrentClient, SpanBuffer } from '../../../../src'; +import { + SentrySpan, + setCurrentClient, + SpanBuffer, + startInactiveSpan, + startSpan, + withStreamedSpan, +} from '../../../../src'; import type { SerializedStreamedSpanWithSegmentSpan } from '../../../../src/tracing/spans/captureSpan'; +import { captureSpan } from '../../../../src/tracing/spans/captureSpan'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; -import { HTTP_METHOD, URL_FULL } from '@sentry/conventions/attributes'; +import { HTTP_METHOD, SENTRY_SEGMENT_NAME, URL_FULL } from '@sentry/conventions/attributes'; describe('SpanBuffer', () => { let client: TestClient; @@ -370,4 +378,123 @@ describe('SpanBuffer', () => { expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); }); }); + + describe('segment name', () => { + function getSentSegmentNames(): Record { + const items = sentEnvelopes[0]?.[1]?.[0]?.[1]?.items ?? []; + return Object.fromEntries(items.map(item => [item.name, item.attributes[SENTRY_SEGMENT_NAME]?.value])); + } + + it('uses the final segment span name for spans that ended before the segment span was renamed', () => { + const buffer = new SpanBuffer(client); + + startSpan({ name: 'GET' }, root => { + startSpan({ name: 'early child' }, span => { + span.end(); + buffer.add(captureSpan(span, client)); + }); + + root.updateName('GET /users/:id'); + + startSpan({ name: 'late child' }, span => { + span.end(); + buffer.add(captureSpan(span, client)); + }); + + root.end(); + buffer.add(captureSpan(root, client)); + }); + + buffer.drain(); + + expect(getSentSegmentNames()).toEqual({ + 'early child': 'GET /users/:id', + 'late child': 'GET /users/:id', + 'GET /users/:id': 'GET /users/:id', + }); + }); + + it('uses the segment span name at flush time if the segment span has not ended yet', () => { + const buffer = new SpanBuffer(client); + + const root = startInactiveSpan({ name: 'GET' }); + const child = startInactiveSpan({ name: 'child', parentSpan: root }); + child.end(); + buffer.add(captureSpan(child, client)); + + root.updateName('GET /users/:id'); + buffer.drain(); + + expect(getSentSegmentNames()).toEqual({ child: 'GET /users/:id' }); + }); + + it('keeps a segment name that differs from the segment span name at capture time', () => { + const buffer = new SpanBuffer(client); + + const root = startInactiveSpan({ name: 'GET' }); + const child = startInactiveSpan({ + name: 'child', + parentSpan: root, + attributes: { [SENTRY_SEGMENT_NAME]: 'custom segment name' }, + }); + child.end(); + buffer.add(captureSpan(child, client)); + + root.updateName('GET /users/:id'); + buffer.drain(); + + expect(getSentSegmentNames()).toEqual({ child: 'custom segment name' }); + }); + + it('keeps a segment name set by beforeSendSpan', () => { + client.getOptions().traceLifecycle = 'stream'; + client.getOptions().beforeSendSpan = withStreamedSpan(span => { + if (span.name === 'child') { + span.attributes = { ...span.attributes, [SENTRY_SEGMENT_NAME]: 'from beforeSendSpan' }; + } + return span; + }); + const buffer = new SpanBuffer(client); + + const root = startInactiveSpan({ name: 'GET' }); + const child = startInactiveSpan({ name: 'child', parentSpan: root }); + child.end(); + buffer.add(captureSpan(child, client)); + + root.updateName('GET /users/:id'); + buffer.drain(); + + expect(getSentSegmentNames()).toEqual({ child: 'from beforeSendSpan' }); + }); + + it("uses the name of each span's own segment span when a trace has several segment spans", () => { + const buffer = new SpanBuffer(client); + + const root1 = startInactiveSpan({ name: 'root 1', forceTransaction: true }); + const root2 = startInactiveSpan({ name: 'root 2', parentSpan: root1, forceTransaction: true }); + const child1 = startInactiveSpan({ name: 'child 1', parentSpan: root1 }); + const child2 = startInactiveSpan({ name: 'child 2', parentSpan: root2 }); + child1.end(); + child2.end(); + buffer.add(captureSpan(child1, client)); + buffer.add(captureSpan(child2, client)); + + root1.updateName('root 1 renamed'); + root2.updateName('root 2 renamed'); + buffer.drain(); + + expect(getSentSegmentNames()).toEqual({ 'child 1': 'root 1 renamed', 'child 2': 'root 2 renamed' }); + }); + + it('does not send the internal segment name field', () => { + const buffer = new SpanBuffer(client); + + const root = startInactiveSpan({ name: 'GET' }); + root.end(); + buffer.add(captureSpan(root, client)); + buffer.drain(); + + expect(sentEnvelopes[0]?.[1]?.[0]?.[1]?.items[0]).not.toHaveProperty('_segmentNameAtCapture'); + }); + }); }); diff --git a/packages/core/test/lib/utils/normalize.test.ts b/packages/core/test/lib/utils/normalize.test.ts index 772abec88525..68a7e8e07979 100644 --- a/packages/core/test/lib/utils/normalize.test.ts +++ b/packages/core/test/lib/utils/normalize.test.ts @@ -588,6 +588,40 @@ describe('normalize()', () => { someFunc: '**non-serializable** (Error: Nope)', }); }); + + test('does not throw when a getter throws a symbol', () => { + const obj = { + get bad(): unknown { + throw Symbol('s'); + }, + }; + + expect(normalize({ bad: obj }, 3)).toEqual({ + ERROR: '**non-serializable** (Symbol(s))', + }); + }); + + test('does not throw when a getter throws a null-prototype object', () => { + const obj = { + get bad(): unknown { + throw Object.create(null); + }, + }; + + expect(normalize({ bad: obj }, 3)).toEqual({ + ERROR: '**non-serializable** (unknown error)', + }); + }); + + test('does not throw when stringifying a value fails with a symbol', () => { + vi.spyOn(stacktraceModule, 'getFunctionName').mockImplementationOnce(() => { + throw Symbol('s'); + }); + + expect(normalize({ someFunc: () => {} })).toEqual({ + someFunc: '**non-serializable** (Symbol(s))', + }); + }); }); test('runs registered stringifier for each visited value', () => { diff --git a/packages/core/test/lib/utils/object.test.ts b/packages/core/test/lib/utils/object.test.ts index 952597f210b3..1fc0af02ef7b 100644 --- a/packages/core/test/lib/utils/object.test.ts +++ b/packages/core/test/lib/utils/object.test.ts @@ -54,6 +54,25 @@ describe('fill()', () => { expect(source.prop()).toEqual(41); }); + test('wraps a method keyed by a symbol', () => { + const key = Symbol('method'); + const source = { [key]: () => 42 }; + + fill(source, key, original => () => original() + 1); + + expect(source[key]()).toBe(43); + }); + + test('does not throw when a method keyed by a symbol is not writeable', () => { + const key = Symbol('method'); + const original = () => 42; + const source = Object.freeze({ [key]: original }); + + fill(source, key, () => () => 1); + + expect(source[key]).toBe(original); + }); + test.each([42, null, undefined, {}])("does't throw if the property is not a function but %s", (propValue: any) => { const source = { foo: propValue, diff --git a/packages/core/test/lib/utils/stacktrace.test.ts b/packages/core/test/lib/utils/stacktrace.test.ts index 2a1700b262a7..17190731ee98 100644 --- a/packages/core/test/lib/utils/stacktrace.test.ts +++ b/packages/core/test/lib/utils/stacktrace.test.ts @@ -232,6 +232,42 @@ describe('node', () => { expect(node(input)).toEqual(expectedOutput); }); + it.each([ + ['at /app/My App (beta)/index.js:4:11', '/app/My App (beta)/index.js'], + ['at async /app/My App (beta)/index.js:4:11', '/app/My App (beta)/index.js'], + ['at C:\\Program Files (x86)\\MyApp\\index.js:4:11', 'C:\\Program Files (x86)\\MyApp\\index.js'], + ['at /app/My App (beta/index.js:4:11', '/app/My App (beta/index.js'], + ['at /app/My App beta)/index.js:4:11', '/app/My App beta)/index.js'], + ])('preserves parentheses in anonymous frame paths: %s', (input, filename) => { + const frame = node(input); + + expect(frame).toEqual({ + filename, + module: undefined, + function: '?', + lineno: 4, + colno: 11, + in_app: true, + }); + }); + + it.each([ + ['at main (/app/My App (beta)/index.js:4:11)', '/app/My App (beta)/index.js'], + ['at main (/app/My App (beta/index.js:4:11)', '/app/My App (beta/index.js'], + ['at main (/app/My App beta)/index.js:4:11)', '/app/My App beta)/index.js'], + ])('preserves parentheses in named frame paths: %s', (input, filename) => { + const frame = node(input); + + expect(frame).toEqual({ + filename, + module: undefined, + function: 'main', + lineno: 4, + colno: 11, + in_app: true, + }); + }); + it('should extract method name and type name correctly', () => { const input = 'at myObject.myMethod (/path/to/file.js:10:5)'; diff --git a/packages/core/test/lib/utils/tunnel.test.ts b/packages/core/test/lib/utils/tunnel.test.ts index f35bbf595bb5..887abaf56718 100644 --- a/packages/core/test/lib/utils/tunnel.test.ts +++ b/packages/core/test/lib/utils/tunnel.test.ts @@ -9,7 +9,7 @@ import { isTracingSuppressed } from '../../../src/tracing/trace'; import { makeDsn } from '../../../src/utils/dsn'; import { createEnvelope, serializeEnvelope } from '../../../src/utils/envelope'; import { - applyTunnelDefaults, + applyBrowserTunnelDefaults, getTunnelPath, getTunnelIgnoreSpanFilters, getTunnelPaths, @@ -23,15 +23,17 @@ import { import type { HandleTunnelRequestOptions } from '../../../src/utils/tunnelForward'; import { forwardTunnelRequest, + handleServedNodeTunnelRequest, handleTunnelRequest, handleTunnelRequestIfMatched, } from '../../../src/utils/tunnelForward'; import { serveTunnelIntegration } from '../../../src/integrations/serveTunnel'; import { - _INTERNAL_resetManagedTunnelDefaults, - applyManagedTunnelDefaults, - setManagedTunnelDefaults, -} from '../../../src/utils/managedTunnelDefaults'; + _INTERNAL_resetTunnelDefaults, + applyServerTunnelDefaults, + enableManagedTunnel, + setTunnelDefaults, +} from '../../../src/utils/tunnelDefaults'; import type { DsnComponents } from '../../../src/types/dsn'; import type { IgnoreSpanFilter, ServerTunnelOptions } from '../../../src/types/options'; import { getDefaultTestClientOptions } from '../../mocks/client'; @@ -956,9 +958,65 @@ describe('getTunnelIgnoreSpanFilter', () => { }); }); +describe('a forward that throws right away', () => { + const throwingForward = (): Promise => { + throw new Error('forward failed'); + }; + + beforeEach(() => { + const client = new ServerRuntimeClient(getDefaultTestClientOptions({ dsn: TEST_DSN })); + setCurrentClient(client); + client.init(); + enableManagedTunnel(client, {}, throwingForward); + }); + + afterEach(() => { + getCurrentScope().setClient(undefined); + }); + + it('ends a claimed `Request` in a 502 instead of handing it to the app', async () => { + const response = handleServedTunnelRequest( + makeEnvelopeRequest({ dsn: TEST_DSN }, `http://localhost${getTunnelPath(TEST_DSN)}`), + ); + + expect((await response)?.status).toBe(502); + }); + + it('ends a claimed Node request in a 502 instead of handing it to the app', async () => { + const server = http.createServer((req, res) => { + if (!handleServedNodeTunnelRequest(req, res)) { + res.end('app'); + } + }); + await new Promise(resolve => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + const envelope = serializeEnvelope(createEnvelope({ dsn: TEST_DSN }, [])) as string; + + try { + // Not `fetch`, which other tests stub. + const status = await new Promise((resolve, reject) => { + const request = http.request( + { port, path: getTunnelPath(TEST_DSN), method: 'POST', headers: { 'content-length': envelope.length } }, + response => { + response.resume(); + resolve(response.statusCode); + }, + ); + request.on('error', reject); + request.end(envelope); + }); + + expect(status).toBe(502); + } finally { + await new Promise(resolve => server.close(resolve)); + } + }); +}); + describe('serveTunnelIntegration', () => { afterEach(() => { getCurrentScope().setClient(undefined); + _INTERNAL_resetTunnelDefaults(); }); it('ignores the server spans of tunnel requests and keeps user filters', () => { @@ -985,7 +1043,7 @@ describe('serveTunnelIntegration', () => { expect(client.getOptions().ignoreSpans).toBeUndefined(); }); - it('serves the tunnel from a separately bundled copy of core, which reaches the forwarder through the client', async () => { + it('serves the tunnel from a separately bundled copy of core through the shared tunnel store', async () => { const client = new ServerRuntimeClient( getDefaultTestClientOptions({ dsn: TEST_DSN, integrations: [serveTunnelIntegration()] }), ); @@ -993,9 +1051,9 @@ describe('serveTunnelIntegration', () => { client.init(); vi.resetModules(); - const bundledDefaults = await import('../../../src/utils/managedTunnelDefaults'); + const bundledDefaults = await import('../../../src/utils/tunnelDefaults'); const bundledTunnel = await import('../../../src/utils/tunnel'); - bundledDefaults.setManagedTunnelDefaults({ basePath: '/app' }); + bundledDefaults.setTunnelDefaults({ basePath: '/app' }); vi.stubGlobal( 'fetch', @@ -1032,6 +1090,18 @@ describe('serveTunnelIntegration', () => { expect(await response?.text()).toBe('forwarded'); }); + it('leaves `intercept: false` tunnel requests to the app, still marked as tunnel requests so they record nothing', () => { + const client = new ServerRuntimeClient( + getDefaultTestClientOptions({ dsn: TEST_DSN, integrations: [serveTunnelIntegration({ intercept: false })] }), + ); + setCurrentClient(client); + client.init(); + const tunnelUrl = `http://localhost${getTunnelPath(TEST_DSN)}`; + + expect(handleServedTunnelRequest(makeEnvelopeRequest({ dsn: TEST_DSN }, tunnelUrl))).toBeUndefined(); + expect(isServedTunnelRequest({ method: 'POST', url: tunnelUrl })).toBe(true); + }); + it('serves nothing for a `_managedTunnel` set without it, since only the integration brings the forwarder', () => { const client = new ServerRuntimeClient(getDefaultTestClientOptions({ dsn: TEST_DSN, _managedTunnel: {} })); setCurrentClient(client); @@ -1044,34 +1114,36 @@ describe('serveTunnelIntegration', () => { }); }); -describe('applyTunnelDefaults', () => { +describe('applyBrowserTunnelDefaults', () => { it('fills in the defaults for `true` and options, and lets options the user set win', () => { const serverTunnel: ServerTunnelOptions = { allowedDsns: [TEST_DSN] }; - expect(applyTunnelDefaults(true, { basePath: '/app' })).toEqual({ basePath: '/app' }); - expect(applyTunnelDefaults(serverTunnel, { basePath: '/app' })).toEqual({ + expect(applyBrowserTunnelDefaults(true, { basePath: '/app' })).toEqual({ basePath: '/app' }); + expect(applyBrowserTunnelDefaults(serverTunnel, { basePath: '/app' })).toEqual({ basePath: '/app', allowedDsns: [TEST_DSN], }); - expect(applyTunnelDefaults({ basePath: '/custom' }, { basePath: '/app' })).toEqual({ basePath: '/custom' }); + expect(applyBrowserTunnelDefaults({ basePath: '/custom' }, { basePath: '/app' })).toEqual({ basePath: '/custom' }); }); it('leaves a path the user wrote as written', () => { - expect(applyTunnelDefaults('/monitor', { basePath: '/app' })).toBe('/monitor'); - expect(applyTunnelDefaults({ path: '/monitor' }, { basePath: '/app' })).toEqual({ path: '/monitor' }); + expect(applyBrowserTunnelDefaults('/monitor', { basePath: '/app' })).toBe('/monitor'); + expect(applyBrowserTunnelDefaults({ path: '/monitor' }, { basePath: '/app' })).toEqual({ path: '/monitor' }); }); it('leaves a URL, an unset tunnel and empty defaults alone', () => { - expect(applyTunnelDefaults('https://example.com/tunnel', { basePath: '/app' })).toBe('https://example.com/tunnel'); - expect(applyTunnelDefaults(undefined, { basePath: '/app' })).toBeUndefined(); - expect(applyTunnelDefaults('/monitor', {})).toBe('/monitor'); - expect(applyTunnelDefaults(true, undefined)).toBe(true); + expect(applyBrowserTunnelDefaults('https://example.com/tunnel', { basePath: '/app' })).toBe( + 'https://example.com/tunnel', + ); + expect(applyBrowserTunnelDefaults(undefined, { basePath: '/app' })).toBeUndefined(); + expect(applyBrowserTunnelDefaults('/monitor', {})).toBe('/monitor'); + expect(applyBrowserTunnelDefaults(true, undefined)).toBe(true); }); it('normalizes the default base path and ignores a root one', () => { - expect(applyTunnelDefaults(true, { basePath: 'app/' })).toEqual({ basePath: '/app' }); - expect(applyTunnelDefaults(true, { basePath: '/' })).toBe(true); - expect(applyTunnelDefaults(true, { basePath: undefined })).toBe(true); + expect(applyBrowserTunnelDefaults(true, { basePath: 'app/' })).toEqual({ basePath: '/app' }); + expect(applyBrowserTunnelDefaults(true, { basePath: '/' })).toBe(true); + expect(applyBrowserTunnelDefaults(true, { basePath: undefined })).toBe(true); }); }); @@ -1090,17 +1162,17 @@ describe('normalizeTunnelBasePath', () => { }); }); -describe('setManagedTunnelDefaults', () => { +describe('setTunnelDefaults', () => { afterEach(() => { getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); }); it('serves the tunnel under the base path and drops its spans there', () => { setTestClient(TEST_DSN, {}); const client = getCurrentScope().getClient()!; - setManagedTunnelDefaults({ basePath: '/app/' }); + setTunnelDefaults({ basePath: '/app/' }); expect(client.getOptions()._managedTunnel).toEqual({ basePath: '/app' }); expect(isTunnelRequest({ method: 'POST', url: `/app${getTunnelPath(TEST_DSN)}` })).toBe(true); @@ -1114,7 +1186,7 @@ describe('setManagedTunnelDefaults', () => { setTestClient(TEST_DSN, { path: '/monitor' }); const client = getCurrentScope().getClient()!; - setManagedTunnelDefaults({ basePath: '/app' }); + setTunnelDefaults({ basePath: '/app' }); expect(client.getOptions()._managedTunnel).toEqual({ path: '/monitor' }); expect(isTunnelRequest({ method: 'POST', url: '/monitor' })).toBe(true); @@ -1124,28 +1196,28 @@ describe('setManagedTunnelDefaults', () => { it('keeps a base path the user set, and does nothing without a managed tunnel or a base path', () => { setTestClient(TEST_DSN, { basePath: '/custom' }); const client = getCurrentScope().getClient()!; - setManagedTunnelDefaults({ basePath: '/app' }); + setTunnelDefaults({ basePath: '/app' }); expect(client.getOptions()._managedTunnel).toEqual({ basePath: '/custom' }); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); setTestClient(TEST_DSN, {}); const rootClient = getCurrentScope().getClient()!; - setManagedTunnelDefaults({ basePath: '/' }); + setTunnelDefaults({ basePath: '/' }); expect(rootClient.getOptions()._managedTunnel).toEqual({}); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); setTestClient(TEST_DSN); const offClient = getCurrentScope().getClient()!; - setManagedTunnelDefaults({ basePath: '/app' }); + setTunnelDefaults({ basePath: '/app' }); expect(offClient.getOptions()._managedTunnel).toBeUndefined(); }); it('enables the tunnel on clients created later, unless their `init` set one', () => { - setManagedTunnelDefaults({ tunnel: { path: '/monitor' }, forward: forwardTunnelRequest, basePath: '/app' }); + setTunnelDefaults({ tunnel: { path: '/monitor' }, forward: forwardTunnelRequest, basePath: '/app' }); setTestClient(TEST_DSN); const laterClient = getCurrentScope().getClient()!; - applyManagedTunnelDefaults(laterClient); + applyServerTunnelDefaults(laterClient); expect(laterClient.getOptions()._managedTunnel).toEqual({ path: '/monitor' }); expect(laterClient.getOptions().ignoreSpans).toContainEqual( getTunnelIgnoreSpanFilter({ path: '/monitor' }, makeDsn(TEST_DSN)), @@ -1153,7 +1225,7 @@ describe('setManagedTunnelDefaults', () => { setTestClient(TEST_DSN, { path: '/from-init' }); const initClient = getCurrentScope().getClient()!; - applyManagedTunnelDefaults(initClient); + applyServerTunnelDefaults(initClient); expect(initClient.getOptions()._managedTunnel).toEqual({ path: '/from-init' }); }); }); diff --git a/packages/deno/package.json b/packages/deno/package.json index 7e18a3598c30..2bbdd0450b69 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -28,7 +28,7 @@ ], "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0" @@ -42,9 +42,9 @@ "build:dev": "pnpm run build", "build:transpile": "pnpm run deno-types && rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build build-types build-test coverage node_modules/.deno sentry-deno-*.tgz", + "clean": "node ../../scripts/rm.mjs build build-types build-test coverage node_modules/.deno sentry-deno-*.tgz", "prefix": "pnpm run deno-types", "prelint": "pnpm run deno-types", "lint": "oxlint . --type-aware", diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 4d5fb75365e7..4cf53f81d690 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -114,11 +114,11 @@ export { export { handleTunnelRequestIfMatched, isTunnelRequest, - serveTunnelIntegration, trpcMiddleware, wrapMcpServerWithSentry, } from '@sentry/core/server'; export type { TunnelRequestLike } from '@sentry/core/server'; +export { serveTunnelIntegration } from './integrations/serveTunnel'; export { getTunnelPath, handleTunnelRequest } from '@sentry/core'; export type { HandleTunnelRequestOptions, ServerTunnelOptions } from '@sentry/core'; export { diff --git a/packages/deno/src/integrations/serveTunnel.ts b/packages/deno/src/integrations/serveTunnel.ts new file mode 100644 index 000000000000..6be5992e3be9 --- /dev/null +++ b/packages/deno/src/integrations/serveTunnel.ts @@ -0,0 +1,31 @@ +import { makeServeTunnelIntegration } from '@sentry/server-utils'; +import { patchDenoServe } from '../tunnelServer'; + +/** + * Lets your server forward the events of a browser SDK set up with `tunnel: true` to Sentry. + * + * With this integration on the server and `tunnel: true` in the browser `init`, the browser sends its events to your + * own server instead of Sentry, where ad-blockers can't drop them, and your server forwards them to Sentry. + * + * `Deno.serve` (including `deno serve`) and `node:http` servers answer the tunnel requests before the app sees them. + * A server started before `init` isn't covered. To serve them from your own code instead, set `intercept: false` and + * call `handleTunnelRequestIfMatched`. + * + * By default only envelopes for the server's own DSN are forwarded. + * + * @example + * ```js + * // Server + * Sentry.init({ + * dsn: '__DSN__', + * integrations: [Sentry.serveTunnelIntegration()], + * }); + * + * // Browser + * Sentry.init({ + * dsn: '__DSN__', + * tunnel: true, + * }); + * ``` + */ +export const serveTunnelIntegration = makeServeTunnelIntegration(patchDenoServe); diff --git a/packages/deno/src/tunnelServer.ts b/packages/deno/src/tunnelServer.ts new file mode 100644 index 000000000000..9c606f38fe67 --- /dev/null +++ b/packages/deno/src/tunnelServer.ts @@ -0,0 +1,65 @@ +import { debug } from '@sentry/core'; +import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest } from '@sentry/core/server'; + +type ServeHandler = (request: Request, info: Deno.ServeHandlerInfo) => Response | Promise; + +let hasPatchedDenoServe = false; + +/** + * Answers tunnel requests ahead of every `Deno.serve` handler. `deno serve` starts the default export through the + * same global, so it's covered too. Nothing is answered unless the tunnel is on. A server started before `init` + * isn't covered. + */ +export function patchDenoServe(): void { + if (hasPatchedDenoServe) { + return; + } + + try { + const originalServe = Deno.serve; + const descriptor = Object.getOwnPropertyDescriptor(Deno, 'serve'); + // `Deno.serve` is a getter, so it can't be assigned. + Object.defineProperty(Deno, 'serve', { + configurable: descriptor?.configurable ?? true, + enumerable: descriptor?.enumerable ?? true, + writable: true, + value: function (this: unknown, ...args: unknown[]) { + return originalServe.apply(this, withTunnelHandler(args)); + }, + }); + hasPatchedDenoServe = true; + } catch (error) { + debug.warn('Sentry tunnel: could not patch Deno.serve', error); + } +} + +/** + * Wraps the handler of `Deno.serve(handler)`, `Deno.serve(options, handler)` or `Deno.serve({ handler })`. Returns + * the arguments as they were if anything goes wrong, so the server still starts. + */ +function withTunnelHandler(args: unknown[]): unknown[] { + try { + const handlerIndex = args.findIndex(arg => typeof arg === 'function'); + if (handlerIndex !== -1) { + const wrapped = [...args]; + wrapped[handlerIndex] = wrapHandler(args[handlerIndex] as ServeHandler); + + return wrapped; + } + + const [options, ...rest] = args as [{ handler?: unknown } | undefined, ...unknown[]]; + if (options && typeof options.handler === 'function') { + return [{ ...options, handler: wrapHandler(options.handler as ServeHandler) }, ...rest]; + } + } catch (error) { + debug.warn('Sentry tunnel: could not wrap the Deno.serve handler', error); + } + + return args; +} + +function wrapHandler(handler: ServeHandler): ServeHandler { + return function (this: unknown, request, info) { + return handleServedTunnelRequest(request) ?? handler.call(this, request, info); + }; +} diff --git a/packages/deno/test/tunnel-hardening.test.ts b/packages/deno/test/tunnel-hardening.test.ts new file mode 100644 index 000000000000..7da6cbb9eebd --- /dev/null +++ b/packages/deno/test/tunnel-hardening.test.ts @@ -0,0 +1,105 @@ +// + +import type { ErrorEvent } from '@sentry/core'; +import { getMainCarrier, getTunnelPath } from '@sentry/core'; +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.212.0/assert/mod.ts'; +import * as http from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { + EXPECTED_TUNNEL_OUTCOMES, + runTunnelHardeningCases, + startFakeIngests, +} from '../../../dev-packages/test-utils/src/tunnel-hardening.ts'; +import { init, serveTunnelIntegration } from '../build/esm/index.js'; + +/** + * Inits the SDK with a tunnel to every fake ingest, and returns the error events it reports. + */ +function initSdk(allowedDsns: string[]): ErrorEvent[] { + getMainCarrier().__SENTRY__ = undefined; + const events: ErrorEvent[] = []; + init({ + dsn: 'https://username@domain/123', + integrations: [serveTunnelIntegration({ allowedDsns })], + beforeSend: event => { + events.push(event); + + return null; + }, + }); + + return events; +} + +/** + * Answers `/health`, and the tunnel path like an app that handles it itself, which the runtime answers before it. + */ +function appStatus(pathname: string, tunnelPath: string): number { + if (pathname === '/health') { + return 200; + } + + return pathname === tunnelPath ? 418 : 404; +} + +async function expectAppUnharmed( + port: number, + ingests: Awaited>, + events: ErrorEvent[], + skip: string[] = [], +): Promise { + const results = await runTunnelHardeningCases({ port, ingests, getTunnelPath: dsn => getTunnelPath(dsn), skip }); + + assertEquals( + results.filter(result => result.health !== '200'), + [], + ); + assertObjectMatch( + Object.fromEntries(results.map(result => [result.name, result.outcome])), + Object.fromEntries(Object.entries(EXPECTED_TUNNEL_OUTCOMES).filter(([name]) => !skip.includes(name))), + ); + // The tunnel reports nothing of its own to the app's project. + assertEquals(events, []); +} + +Deno.test('managed tunnel hardening: Deno.serve', async () => { + const ingests = await startFakeIngests(); + const tunnelPath = getTunnelPath(ingests.dsn('ok'))!; + const events = initSdk(ingests.allDsns()); + + const abortController = new AbortController(); + let onListen: ((_: unknown) => void) | undefined = undefined; + const listening = new Promise(resolve => (onListen = resolve)); + const server = Deno.serve({ port: 0, signal: abortController.signal, onListen }, request => { + return new Response('app', { status: appStatus(new URL(request.url).pathname, tunnelPath) }); + }); + await listening; + + try { + // `Deno.serve` resets a connection it answered before reading a large body, Sentry or not. + await expectAppUnharmed(server.addr.port, ingests, events, ['rejected tunnel with a big unread body, then app']); + } finally { + abortController.abort(); + await server.finished; + await ingests.close(); + } +}); + +Deno.test('managed tunnel hardening: node:http', async () => { + const ingests = await startFakeIngests(); + const tunnelPath = getTunnelPath(ingests.dsn('ok'))!; + const events = initSdk(ingests.allDsns()); + + const server = http.createServer((request, response) => { + response.writeHead(appStatus(request.url ?? '/', tunnelPath)).end('app'); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + + try { + await expectAppUnharmed((server.address() as AddressInfo).port, ingests, events); + } finally { + server.closeAllConnections(); + await new Promise(resolve => server.close(resolve)); + await ingests.close(); + } +}); diff --git a/packages/effect/package.json b/packages/effect/package.json index 3fc4f30e7425..474eaf41bcac 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0" }, @@ -70,9 +70,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-effect-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-effect-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/elysia/package.json b/packages/elysia/package.json index d4ba6ac3a7f9..ad8d65780d1f 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -34,7 +34,7 @@ "dependencies": { "@sentry/bun": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "elysia": "^1.4.0" @@ -57,9 +57,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-elysia-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-elysia-*.tgz", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/ember/package.json b/packages/ember/package.json index e8f1d201a9af..779dde1cc708 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -28,10 +28,10 @@ "scripts": { "build": "rollup --config", "build:dev": "pnpm run build", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "build:watch": "rollup --config --watch", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf dist declarations *.tgz .turbo", + "clean": "node ../../scripts/rm.mjs dist declarations *.tgz .turbo", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", @@ -45,7 +45,7 @@ "@embroider/addon-shim": "^1.11.0", "@embroider/macros": "^1.20.2", "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "decorator-transforms": "^2.3.2" }, @@ -56,14 +56,13 @@ "@ember/app-tsconfig": "^2.0.0", "@ember/library-tsconfig": "^2.0.0", "@embroider/addon-dev": "^8.3.0", - "@embroider/compat": "^4.1.17", "@embroider/core": "^4.4.7", "@embroider/vite": "^1.7.2", "@glint/ember-tsc": "^1.5.0", "@rollup/plugin-babel": "^7.0.0", "ember-source": "^6.12.0", "typescript": "~5.8.0", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "engines": { "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" diff --git a/packages/esbuild-plugin/package.json b/packages/esbuild-plugin/package.json index 882c2bb67e9e..abdd3c4fd5a3 100644 --- a/packages/esbuild-plugin/package.json +++ b/packages/esbuild-plugin/package.json @@ -40,8 +40,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-esbuild-plugin-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-esbuild-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", "test": "vitest run", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 15510c3fe989..8920272e1dea 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -39,8 +39,8 @@ "eslint": "8.57.0" }, "scripts": { - "clean": "pnpm exec rimraf sentry-eslint-config-sdk-*.tgz", - "build:tarball": "npm pack", + "clean": "node ../../scripts/rm.mjs sentry-eslint-config-sdk-*.tgz", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.js" } } diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 50c4fbd72333..5cfe013780df 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -26,13 +26,13 @@ "eslint": "^8.57.0" }, "scripts": { - "clean": "pnpm exec rimraf sentry-eslint-plugin-sdk-*.tgz", + "clean": "node ../../scripts/rm.mjs sentry-eslint-plugin-sdk-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", "test": "vitest run", "test:watch": "vitest --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.js" } } diff --git a/packages/feedback/package.json b/packages/feedback/package.json index da9ba00faa6e..fb93ba3d96b7 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -49,9 +49,9 @@ "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "pnpm run build:transpile --watch", "build:bundle:watch": "pnpm run build:bundle --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build sentry-feedback-*.tgz", + "clean": "node ../../scripts/rm.mjs build sentry-feedback-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index e60d029a1945..26031bda2b06 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -48,7 +48,6 @@ }, "devDependencies": { "@sentry-internal/rollup-utils": "11.11.0", - "@testing-library/react": "^15.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", "webpack": "^5.112.2" @@ -63,9 +62,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage *.d.ts sentry-gatsby-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage *.d.ts sentry-gatsby-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index d85aebf1039e..d3460a70747f 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -36,7 +36,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", @@ -66,9 +66,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-google-cloud-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-google-cloud-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/hono/package.json b/packages/hono/package.json index 9566f0d14e25..82fc5547f79f 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -74,7 +74,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/server-utils": "11.11.0" }, "peerDependencies": { @@ -111,8 +111,7 @@ "@sentry/node": "11.11.0", "@types/node": "^20.19.0", "hono": "4.13.11", - "hono-5": "npm:hono@5.0.0-rc.1", - "wrangler": "4.156.0" + "hono-5": "npm:hono@5.0.0-rc.1" }, "scripts": { "build": "run-p build:transpile build:types", @@ -122,9 +121,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-hono-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-hono-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/hono/test/shared/middlewareHandlers.test.ts b/packages/hono/test/shared/middlewareHandlers.test.ts index fb7d16699d07..2ab3f046c594 100644 --- a/packages/hono/test/shared/middlewareHandlers.test.ts +++ b/packages/hono/test/shared/middlewareHandlers.test.ts @@ -50,13 +50,17 @@ const getRootSpanMock = SentryCore.getRootSpan as ReturnType; // `event` throws on access so `hasFetchEvent` reports `false` (the Node/Bun/Deno path), which makes // the request handler read request data from `req.raw` rather than a Cloudflare fetch event. -function createMockContext(status: number, error?: Error): object { +function createMockContext( + status: number, + error?: Error, + matchedRoutes: unknown[] = [{ basePath: '/', path: '/test', method: 'GET', handler: (_c: unknown) => undefined }], +): object { return { req: { method: 'GET', routeIndex: 0, // `resolveRouteName` reads the matched routes straight off the request. - matchedRoutes: [{ basePath: '/', path: '/test', method: 'GET', handler: (_c: unknown) => undefined }], + matchedRoutes, raw: new Request('http://localhost/test'), }, get event(): never { @@ -204,6 +208,16 @@ describe('responseHandler (via createHonoRequestMiddleware)', () => { expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(HTTP_ROUTE, '/test'); expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SENTRY_SEGMENT_NAME_SOURCE, 'route'); }); + + it('keeps the runtime name when no route matched', async () => { + getActiveSpanMock.mockReturnValue(mockRootSpan); + + await runMiddleware(createMockContext(404, undefined, [])); + + expect(mockRootSpan.updateName).not.toHaveBeenCalled(); + expect(mockRootSpan.setAttribute).not.toHaveBeenCalledWith(HTTP_ROUTE, expect.anything()); + expect(mockSetTransactionName).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/hono/test/shared/resolveRouteName.test.ts b/packages/hono/test/shared/resolveRouteName.test.ts new file mode 100644 index 000000000000..badb5b1c9e0e --- /dev/null +++ b/packages/hono/test/shared/resolveRouteName.test.ts @@ -0,0 +1,234 @@ +import * as SentryCore from '@sentry/core'; +import { applyHonoPatches, createHonoRequestMiddleware } from '@sentry/server-utils'; +import { Hono, type MiddlewareHandler, type Next } from 'hono'; +import { HonoRequest } from 'hono/request'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const mockSetTransactionName = vi.fn(); + +vi.mock('@sentry/core', async () => { + const actual = await vi.importActual('@sentry/core'); + return { + ...actual, + getActiveSpan: vi.fn(() => undefined), + getIsolationScope: vi.fn(() => ({ + setTransactionName: mockSetTransactionName, + setSDKProcessingMetadata: vi.fn(), + })), + }; +}); + +const getIsolationScopeMock = SentryCore.getIsolationScope as ReturnType; + +// One isolation scope per request, as in a server. A shared scope keeps the dedup marker of the +// Sentry middleware, which then skips the next request. +function freshIsolationScope(): void { + getIsolationScopeMock.mockReturnValue({ + setTransactionName: mockSetTransactionName, + setSDKProcessingMetadata: vi.fn(), + }); +} + +function createApp({ + restoreRouteIndex, + middleware, +}: { + restoreRouteIndex: boolean; + middleware?: { path: string; handler: MiddlewareHandler }; +}): Hono { + const app = new Hono(); + applyHonoPatches(app); + app.use(createHonoRequestMiddleware({ runtime: 'node' })); + + if (restoreRouteIndex) { + // Like Hono 5, which resets `routeIndex` after `next()` (honojs/hono#4424). + app.use(async (c, next) => { + const routeIndex = c.req.routeIndex; + await next(); + c.req.routeIndex = routeIndex; + }); + } + + app.use('/users/*', async (_c, next) => { + await next(); + }); + // Last in its method+path group, so that only its arity tells if it is a middleware. + if (middleware) { + app.use(middleware.path, middleware.handler); + } + // A route handler from `.use()`, which also gets a middleware span wrapper. + app.use('/users/via-use', async c => c.text('via use')); + app.get('/users/me', c => c.text('me')); + app.get('/users/:id', c => c.text('user')); + app.use('/mw-only/*', async (_c, _next) => new Response('handled by middleware')); + + // A middleware that returns early. Of the matched route handlers, Hono would have run `/blocked/me`. + app.use('/blocked/*', async (c, _next) => c.text('blocked', 401)); + app.get('/blocked/me', c => c.text('me')); + app.get('/blocked/:id', c => c.text('user')); + app.get('/blocked/*', c => c.text('catch-all')); + + const subApp = new Hono(); + subApp.get('/items/:itemId', c => c.text('item')); + app.route('/sub', subApp); + + // Passes `draft` on to the catch-all below, which then names the request. + app.get('/posts/:id', async (c, next) => { + if (c.req.param('id') === 'draft') { + await next(); + return; + } + return c.text('post'); + }); + app.get('/posts/*', c => c.text('fallback')); + + // A catch-all registered last. It also matches `/users/me` and `/users/42`, but never responds. + app.get('/users/*', c => c.text('catch-all')); + return app; +} + +// Returns a function that restores the property. +function overrideRequestProperty(key: PropertyKey, descriptor: PropertyDescriptor): () => void { + const original = Object.getOwnPropertyDescriptor(HonoRequest.prototype, key); + Object.defineProperty(HonoRequest.prototype, key, { configurable: true, ...descriptor }); + return () => { + if (original) { + Object.defineProperty(HonoRequest.prototype, key, original); + } + }; +} + +const hideRequestProperty = (key: PropertyKey): (() => void) => overrideRequestProperty(key, { value: undefined }); + +// The symbol of the match result getter, which `hono/route` reads. +const [matchResultKey] = Object.getOwnPropertySymbols(HonoRequest.prototype); +if (!matchResultKey) { + throw new Error('The match result getter is missing on `HonoRequest.prototype`.'); +} + +const SOURCES: Array<{ source: string; hidden: PropertyKey[]; restoreRouteIndex: boolean }> = [ + { source: 'Hono 4', hidden: [], restoreRouteIndex: false }, + // Hono 5 has no `c.req.matchedRoutes`. + { source: 'Hono 5 (simulated)', hidden: ['matchedRoutes'], restoreRouteIndex: true }, +]; + +const CASES: Array<[path: string, expected: string]> = [ + ['/users/me', 'GET /users/me'], + ['/users/42', 'GET /users/:id'], + ['/users/via-use', 'GET /users/via-use'], + ['/users/all/the/things', 'GET /users/*'], + ['/sub/items/7', 'GET /sub/items/:itemId'], + ['/mw-only/anything', 'GET /mw-only/*'], + ['/blocked/me', 'GET /blocked/me'], + ['/posts/1', 'GET /posts/:id'], + ['/posts/draft', 'GET /posts/*'], +]; + +const noop = async (): Promise => {}; + +// Arity does not count rest parameters or a parameter with a default, so these look like route handlers. +const MIDDLEWARE_WITHOUT_NEXT_ARITY: Array<{ description: string; path: string; handler: MiddlewareHandler }> = [ + { description: 'rest parameters', path: '/*', handler: (_c, ...rest: [Next]) => rest[0]() }, + { + description: 'a default `next`', + path: '/*', + handler: async (_c, next: Next = noop) => { + await next(); + }, + }, + { description: 'rest parameters on a path', path: '/users/*', handler: (_c, ...rest: [Next]) => rest[0]() }, +]; + +describe.each(SOURCES)('route name on $source', ({ hidden, restoreRouteIndex }) => { + let restores: Array<() => void> = []; + + beforeEach(() => { + vi.clearAllMocks(); + restores = hidden.map(hideRequestProperty); + }); + + afterEach(() => { + restores.forEach(restore => restore()); + }); + + it.each(CASES)('names %s as %s', async (path, expected) => { + freshIsolationScope(); + const app = createApp({ restoreRouteIndex }); + + await app.request(path); + + expect(mockSetTransactionName).toHaveBeenLastCalledWith(expected); + }); + + it.each(MIDDLEWARE_WITHOUT_NEXT_ARITY)( + 'names the route behind a middleware with $description', + async ({ path, handler }) => { + freshIsolationScope(); + const app = createApp({ restoreRouteIndex, middleware: { path, handler } }); + + await app.request('/users/42'); + + expect(mockSetTransactionName).toHaveBeenLastCalledWith('GET /users/:id'); + }, + ); + + it('does not name a request without a route', async () => { + freshIsolationScope(); + const app = createApp({ restoreRouteIndex }); + + await app.request('/does-not-exist'); + + expect(mockSetTransactionName).not.toHaveBeenCalled(); + }); +}); + +describe('route name with another symbol-keyed getter on the request', () => { + let restores: Array<() => void> = []; + + beforeEach(() => { + vi.clearAllMocks(); + restores = [ + hideRequestProperty('matchedRoutes'), + overrideRequestProperty(Symbol('requestId'), { get: () => 'req-7f3a' }), + ]; + }); + + afterEach(() => { + restores.forEach(restore => restore()); + }); + + it('still finds the match result getter', async () => { + freshIsolationScope(); + const app = createApp({ restoreRouteIndex: true }); + + await app.request('/users/me'); + + expect(mockSetTransactionName).toHaveBeenLastCalledWith('GET /users/me'); + }); +}); + +describe('route name from a match result of an unknown shape', () => { + let restores: Array<() => void> = []; + + beforeEach(() => { + vi.clearAllMocks(); + restores = [ + hideRequestProperty('matchedRoutes'), + overrideRequestProperty(matchResultKey, { get: () => [[[null]], {}] }), + ]; + }); + + afterEach(() => { + restores.forEach(restore => restore()); + }); + + it('keeps the request working and does not name it', async () => { + freshIsolationScope(); + const app = createApp({ restoreRouteIndex: true }); + + const response = await app.request('/users/me'); + + expect(response.status).toBe(200); + expect(mockSetTransactionName).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 8ee47e7f0010..5db06621ef9d 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -31,7 +31,7 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "run-p build:watch", "build:transpile:watch": "pnpm run build:transpile --watch", - "clean": "rimraf build", + "clean": "node ../../scripts/rm.mjs build", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 4f9309508958..c6a59ebe66da 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -54,7 +54,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-utils": "11.11.0" @@ -80,9 +80,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.ts && madge --circular src/setup.ts", - "clean": "rimraf build coverage sentry-nestjs-*.tgz ./*.d.ts ./*.d.ts.map", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.ts src/setup.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-nestjs-*.tgz ./*.d.ts ./*.d.ts.map", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index d94711c5eac6..f7f4e6ac0a8b 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -87,7 +87,7 @@ "@sentry/browser-utils": "11.11.0", "@sentry/bundler-plugins": "11.11.0", "@sentry/cloudflare": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/opentelemetry": "11.11.0", @@ -116,9 +116,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/edge/index.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-nextjs-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/edge/index.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-nextjs-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index f2f933bc11bf..5a20f7b68b76 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -26,10 +26,20 @@ import { } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; +/** + * Unlike `getAbsoluteUrl`, keeps a `//host/path` target protocol-relative: Next.js treats such an + * href as an external URL and navigates to that host. + */ +function getNavigationTargetUrl(href: string): string { + // `router.push()` arguments reach this untyped, so a non-string must not throw here. + const isProtocolRelative = typeof href === 'string' && href.startsWith('//'); + return getAbsoluteUrl(isProtocolRelative ? `${WINDOW.location.protocol}${href}` : href); +} + function setNavigationSpanUrlAttributes(span: Span, urlPath: string, urlOrPath: string): void { span.setAttributes({ [URL_PATH]: urlPath, - [URL_FULL]: filterCollectedUrl(getAbsoluteUrl(urlOrPath)), + [URL_FULL]: filterCollectedUrl(getNavigationTargetUrl(urlOrPath)), }); } @@ -170,7 +180,7 @@ export function appRouterInstrumentNavigation(client: Client): void { [URL_TEMPLATE]: parameterizedPathname, }, }, - { url: getAbsoluteUrl(normalizedHref) }, + { url: getNavigationTargetUrl(normalizedHref) }, ); } }; @@ -306,7 +316,7 @@ function patchRouter(client: Client, router: NextRouter, currentNavigationSpanRe [URL_TEMPLATE]: parameterizedPathname, }, }, - { url: getAbsoluteUrl(normalizedHref) }, + { url: getNavigationTargetUrl(normalizedHref) }, ); return target.apply(thisArg, argArray); diff --git a/packages/nextjs/src/client/routing/traceMetaTags.ts b/packages/nextjs/src/client/routing/traceMetaTags.ts index 0d85189e8b4b..c276a8cef03e 100644 --- a/packages/nextjs/src/client/routing/traceMetaTags.ts +++ b/packages/nextjs/src/client/routing/traceMetaTags.ts @@ -1,4 +1,4 @@ -import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; import type { Span, SpanLink, StartSpanOptions } from '@sentry/core'; import { extractTraceparentData, GLOBAL_OBJ } from '@sentry/core'; import { CACHE_ORIGIN_META_TAG_NAME } from '../../common/traceMetaTags'; @@ -16,9 +16,6 @@ export interface TraceMetaTagValues { baggage: string | undefined; } -// Not in `@sentry/conventions` yet: https://github.com/getsentry/sentry-conventions/pull/740 -const PAGE_SERVER_RENDERING_TYPE = 'page.server_rendering.type'; - function getDocument(): Document | undefined { return WINDOW.document; } diff --git a/packages/nextjs/src/config/withSentryConfig/buildTime.ts b/packages/nextjs/src/config/withSentryConfig/buildTime.ts index 836a3829ea5d..3dfe1648982a 100644 --- a/packages/nextjs/src/config/withSentryConfig/buildTime.ts +++ b/packages/nextjs/src/config/withSentryConfig/buildTime.ts @@ -86,7 +86,7 @@ export function getGitRevision(): string | undefined { let gitRevision: string | undefined; try { gitRevision = childProcess - .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }) + .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }) .toString() .trim(); } catch { diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index baaed91053d2..acfadf8aea92 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -37,6 +37,7 @@ export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; // Different implementation in server and worker export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration; +export declare const serveTunnelIntegration: typeof serverSdk.serveTunnelIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts b/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts index 25ecbfdd3f80..76817198cd5a 100644 --- a/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts +++ b/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts @@ -1,5 +1,11 @@ // @vitest-environment jsdom -import { SENTRY_LINK_TYPE, SENTRY_OP, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { + PAGE_SERVER_RENDERING_TYPE, + SENTRY_LINK_TYPE, + SENTRY_OP, + URL_FULL, + URL_PATH, +} from '@sentry/conventions/attributes'; import { PAGELOAD } from '@sentry/conventions/op'; import type { Client } from '@sentry/core'; import type * as SentryCore from '@sentry/core'; @@ -196,6 +202,26 @@ describe('appRouterInstrumentNavigation (router-patch mode)', () => { expect(core.spanToJSON(pushSpan!).end_timestamp).toBeDefined(); }); + it('records a protocol-relative `router.push()` target as an external URL', async () => { + const { core, router } = await setup(traceLifecycle); + + router.push('//cdn.example.com/x'); + + expect(core.spanToJSON(core.getActiveSpan()!).attributes).toEqual( + expect.objectContaining({ [URL_FULL]: 'http://cdn.example.com/x' }), + ); + }); + + it('records a protocol-relative transition-start-hook target as an external URL', async () => { + const { core, instrumentation } = await setup(traceLifecycle); + + instrumentation.captureRouterTransitionStart('//cdn.example.com/x', 'push'); + + expect(core.spanToJSON(core.getActiveSpan()!).attributes).toEqual( + expect.objectContaining({ [URL_FULL]: 'http://cdn.example.com/x' }), + ); + }); + it('tags a popstate without a preceding router call with `browser.popstate`', async () => { const { core } = await setup(traceLifecycle); @@ -260,6 +286,16 @@ describe('appRouterInstrumentNavigation with basePath', () => { ); }); + it('records a `//` target as a path under basePath, like Next.js does', async () => { + const { core, router } = await setup('static'); + + router.push('//cdn.example.com/x'); + + expect(core.spanToJSON(core.getActiveSpan()!).attributes).toEqual( + expect.objectContaining({ [URL_FULL]: 'http://localhost:3000/my-app//cdn.example.com/x' }), + ); + }); + it.each([ ['a root-relative path without basePath', '/navigation/42/router-push'], ['a root-relative path with basePath', '/my-app/navigation/42/router-push'], @@ -375,7 +411,7 @@ describe('appRouterInstrumentPageLoad', () => { const spanJson = core.spanToJSON(core.getActiveSpan()!); expect(spanJson.attributes?.[SENTRY_OP]).toBe(PAGELOAD); - expect(spanJson.attributes?.['page.server_rendering.type']).toBe('static'); + expect(spanJson.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toBe('static'); expect(spanJson.trace_id).not.toBe(PRERENDER_TRACE_ID); expect(spanJson.links).toEqual([PRERENDER_LINK]); }); @@ -393,7 +429,7 @@ describe('appRouterInstrumentPageLoad', () => { const spanJson = core.spanToJSON(core.getActiveSpan()!); expect(spanJson.trace_id).toBe('22222222222222222222222222222222'); expect(spanJson.parent_span_id).toBe('2222222222222222'); - expect(spanJson.attributes?.['page.server_rendering.type']).toBe('partially_static'); + expect(spanJson.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toBe('partially_static'); expect(spanJson.links).toEqual([PRERENDER_LINK]); }); @@ -406,7 +442,7 @@ describe('appRouterInstrumentPageLoad', () => { const spanJson = core.spanToJSON(core.getActiveSpan()!); expect(spanJson.attributes?.[SENTRY_OP]).toBe(PAGELOAD); - expect(spanJson.attributes).not.toHaveProperty('page.server_rendering.type'); + expect(spanJson.attributes).not.toHaveProperty(PAGE_SERVER_RENDERING_TYPE); expect(spanJson.links).toEqual([PRERENDER_LINK]); }); diff --git a/packages/nextjs/test/client/traceMetaTags.test.ts b/packages/nextjs/test/client/traceMetaTags.test.ts index 695ad7c558bb..ec821cc2312f 100644 --- a/packages/nextjs/test/client/traceMetaTags.test.ts +++ b/packages/nextjs/test/client/traceMetaTags.test.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { SENTRY_LINK_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; import { PAGELOAD } from '@sentry/conventions/op'; import type { Span, StartSpanOptions } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -240,7 +240,7 @@ describe('withCacheOrigin', () => { expect(withCacheOrigin(PAGELOAD_OPTIONS)).toEqual({ name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD, 'page.server_rendering.type': 'static' }, + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, links: [PRERENDER_LINK], }); }); @@ -250,7 +250,7 @@ describe('withCacheOrigin', () => { expect(withCacheOrigin(PAGELOAD_OPTIONS, { sentryTrace: BODY_TRACE, baggage: undefined })).toEqual({ name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD, 'page.server_rendering.type': 'partially_static' }, + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'partially_static' }, links: [PRERENDER_LINK], }); }); @@ -277,7 +277,7 @@ describe('withCacheOrigin', () => { expect(withCacheOrigin(PAGELOAD_OPTIONS)).toEqual({ name: '/products/:id', - attributes: { [SENTRY_OP]: PAGELOAD, 'page.server_rendering.type': 'static' }, + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, }); }); }); diff --git a/packages/nextjs/test/config/buildTime.test.ts b/packages/nextjs/test/config/buildTime.test.ts new file mode 100644 index 000000000000..ffce5e7523a1 --- /dev/null +++ b/packages/nextjs/test/config/buildTime.test.ts @@ -0,0 +1,20 @@ +import * as childProcess from 'child_process'; +import { afterEach, expect, it, vi } from 'vitest'; +import { getGitRevision } from '../../src/config/withSentryConfig/buildTime'; + +vi.mock('child_process', { spy: true }); + +afterEach(() => { + vi.clearAllMocks(); +}); + +it('hides the console when detecting the git revision', () => { + const revision = '4316c2634a5f3de43cbd63780117d9221a23e3cd'; + const execSpy = vi.mocked(childProcess.execSync).mockReturnValueOnce(Buffer.from(`${revision}\n`)); + + expect(getGitRevision()).toBe(revision); + expect(execSpy).toHaveBeenCalledWith('git rev-parse HEAD', { + stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, + }); +}); diff --git a/packages/nitro/package.json b/packages/nitro/package.json index 69c6d2a68c65..979cdca2f177 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@sentry/bundler-plugins": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", @@ -46,8 +46,7 @@ "@sentry-internal/rollup-utils": "11.11.0", "h3": "^2.0.1-rc.20", "nitro": "^3.0.260415-beta", - "srvx": "^0.11.22", - "unstorage": "^2.0.0-alpha.7" + "srvx": "^0.11.22" }, "scripts": { "build": "run-p build:transpile build:types", @@ -59,8 +58,8 @@ "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-nitro-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-nitro-*.tgz", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/nitro/src/module.ts b/packages/nitro/src/module.ts index 16ef233a809d..f766690197e5 100644 --- a/packages/nitro/src/module.ts +++ b/packages/nitro/src/module.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults } from '@sentry/core/server'; +import { _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server'; import type { NitroModule } from 'nitro/types'; import type { SentryNitroOptions } from './config'; import { instrumentServer } from './instruments/instrumentServer'; @@ -19,7 +19,7 @@ export function createNitroModule(sentryOptions?: SentryNitroOptions, sentryEnab if (nitro.options.dev) { // `sentryTunnelBasePath` is set by `@sentry/solidstart`, whose app is served under Vite's `base`. const tunnelBasePath = nitro.options.runtimeConfig.sentryTunnelBasePath as string | undefined; - setManagedTunnelDefaults({ basePath: tunnelBasePath || nitro.options.baseURL }); + setTunnelDefaults({ basePath: tunnelBasePath || nitro.options.baseURL }); } }, }; diff --git a/packages/nitro/src/runtime/plugins/server.ts b/packages/nitro/src/runtime/plugins/server.ts index 286446ed90c0..5f5827812e5e 100644 --- a/packages/nitro/src/runtime/plugins/server.ts +++ b/packages/nitro/src/runtime/plugins/server.ts @@ -1,7 +1,4 @@ -import { - _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, -} from '@sentry/core/server'; +import { _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server'; import { definePlugin } from 'nitro'; import { useRuntimeConfig } from 'nitro/runtime-config'; // @ts-expect-error - This is a virtual module @@ -13,13 +10,8 @@ export default definePlugin(nitroApp => { nitroApp.hooks.hook('error', captureErrorHook); instrumentTasks(tasks); - // Nitro runs the app's `middleware/` before anything a module registers, so an auth middleware there would block - // the tunnel. Every preset hands requests to `nitroApp.fetch` after the plugins ran, so wrapping it goes first, - // before route rules and middleware. Requests still carry Nitro's `baseURL` here, so the tunnel is served under it, - // unless a framework serves the app under another prefix: `@sentry/solidstart` sets `sentryTunnelBasePath` to Vite's - // `base`. + // The runtime answers managed tunnel requests, under Nitro's `baseURL` unless a framework serves the app under + // another prefix: `@sentry/solidstart` sets `sentryTunnelBasePath` to Vite's `base`. const runtimeConfig = useRuntimeConfig() as { app?: { baseURL?: string }; sentryTunnelBasePath?: string }; - setManagedTunnelDefaults({ basePath: runtimeConfig.sentryTunnelBasePath || runtimeConfig.app?.baseURL }); - const appFetch = nitroApp.fetch; - nitroApp.fetch = request => handleServedTunnelRequest(request) ?? appFetch(request); + setTunnelDefaults({ basePath: runtimeConfig.sentryTunnelBasePath || runtimeConfig.app?.baseURL }); }); diff --git a/packages/nitro/test/module.test.ts b/packages/nitro/test/module.test.ts index c293176a5bd8..da94441fa09f 100644 --- a/packages/nitro/test/module.test.ts +++ b/packages/nitro/test/module.test.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/core'; import { getCurrentScope, makeDsn } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; +import { _INTERNAL_resetTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createNitroModule } from '../src/module'; @@ -26,7 +26,7 @@ function setup(options: { dev: boolean; baseURL: string; runtimeConfig?: Record< describe('createNitroModule', () => { afterEach(() => { getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); }); it("serves the dev server's managed tunnel under `baseURL`", () => { diff --git a/packages/node-native/package.json b/packages/node-native/package.json index f114ab17c789..d1f2921a70c3 100644 --- a/packages/node-native/package.json +++ b/packages/node-native/package.json @@ -52,7 +52,7 @@ "build:dev": "pnpm clean && pnpm run build", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:watch": "run-p build:transpile:watch", - "build:tarball": "npm pack" + "build:tarball": "pnpm pack" }, "dependencies": { "@sentry/node-native-stacktrace": "^0.5.1", diff --git a/packages/node/package.json b/packages/node/package.json index 8e03e232e2de..a50468cc5743 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -78,7 +78,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/opentelemetry": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", @@ -97,9 +97,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-node-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-node-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 859d3b02719a..0e0250f6c432 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -186,11 +186,11 @@ export { export { handleTunnelRequestIfMatched, isTunnelRequest, - serveTunnelIntegration, trpcMiddleware, wrapMcpServerWithSentry, } from '@sentry/core/server'; export type { TunnelRequestLike } from '@sentry/core/server'; +export { installTunnelHooks as _INTERNAL_installTunnelHooks, serveTunnelIntegration } from './integrations/serveTunnel'; export { getTunnelPath, handleTunnelRequest } from '@sentry/core'; export type { HandleTunnelRequestOptions, ServerTunnelOptions } from '@sentry/core'; diff --git a/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts index d341a3d9ffa3..b878fff7f79d 100644 --- a/packages/node/src/integrations/fs/vendored/instrumentation.ts +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -25,12 +25,22 @@ import { suppressTracing, withActiveSpan, } from '@sentry/core'; -import * as fs from 'fs'; +import * as fsNamespace from 'fs'; +import * as moduleNamespace from 'module'; import { promisify } from 'util'; import { CALLBACK_FUNCTIONS, PROMISE_FUNCTIONS, SYNC_FUNCTIONS } from './constants'; import type { FMember, FPMember, FsInstrumentationConfig, GenericFunction } from './types'; import { indexFs } from './utils'; +// In native ESM, `import * as fs` is a read-only module namespace, so we patch the mutable `fs` object +// behind its default export instead. The CJS build has no `.default` and already gets the mutable object. +const fs = (fsNamespace as { default?: typeof fsNamespace }).default ?? fsNamespace; + +// Deno before 2.8.2 has `syncBuiltinESMExports` only on the default export of `node:module`, so a named import fails to link. +type SyncBuiltinESMExports = { syncBuiltinESMExports?: () => void }; +const moduleExports = moduleNamespace as unknown as SyncBuiltinESMExports & { default?: SyncBuiltinESMExports }; +const syncBuiltinESMExports = moduleExports.default?.syncBuiltinESMExports ?? moduleExports.syncBuiltinESMExports; + const SPAN_ORIGIN = 'auto.file.fs'; const SPAN_OP = FILE; @@ -304,6 +314,13 @@ export function enableFsInstrumentation(config: FsInstrumentationConfig = {}): v for (const fName of PROMISE_FUNCTIONS) { _patchMethod(fsPromises, fName, original => _patchPromiseFunction(fName, original, config)); } + + // Named ESM imports (`import { readFile } from 'fs'`) are snapshots of the CJS exports until synced. + try { + syncBuiltinESMExports?.(); + } catch { + // Cloudflare workerd exports `syncBuiltinESMExports` but throws `ERR_METHOD_NOT_IMPLEMENTED` when it is called. + } } /** diff --git a/packages/node/src/integrations/serveTunnel.ts b/packages/node/src/integrations/serveTunnel.ts new file mode 100644 index 000000000000..9a799638563b --- /dev/null +++ b/packages/node/src/integrations/serveTunnel.ts @@ -0,0 +1,40 @@ +import { + _INTERNAL_instrumentTunnelServers as instrumentTunnelServers, + makeServeTunnelIntegration, +} from '@sentry/server-utils'; + +/** + * Lets every `node:http` server answer managed tunnel requests before the app sees them. Nothing is answered until the + * tunnel is on. For framework SDKs that enable the tunnel after `init`, like TanStack Start's `tunnelRoute`. + */ +export function installTunnelHooks(): void { + instrumentTunnelServers(); +} + +/** + * Lets your server forward the events of a browser SDK set up with `tunnel: true` to Sentry. + * + * With this integration on the server and `tunnel: true` in the browser `init`, the browser sends its events to your + * own server instead of Sentry, where ad-blockers can't drop them, and your server forwards them to Sentry. + * + * Every `node:http` server answers the tunnel requests before the app sees them. To serve them from your own code + * instead, set `intercept: false` and call `handleTunnelRequestIfMatched`. + * + * By default only envelopes for the server's own DSN are forwarded. + * + * @example + * ```js + * // Server + * Sentry.init({ + * dsn: '__DSN__', + * integrations: [Sentry.serveTunnelIntegration()], + * }); + * + * // Browser + * Sentry.init({ + * dsn: '__DSN__', + * tunnel: true, + * }); + * ``` + */ +export const serveTunnelIntegration = makeServeTunnelIntegration(); diff --git a/packages/node/src/integrations/tracing/bullmq/meter.ts b/packages/node/src/integrations/tracing/bullmq/meter.ts index bfae7aa342f8..ba5d6f19771c 100644 --- a/packages/node/src/integrations/tracing/bullmq/meter.ts +++ b/packages/node/src/integrations/tracing/bullmq/meter.ts @@ -1,4 +1,5 @@ import { MESSAGING_OPERATION_NAME, MESSAGING_SYSTEM } from '@sentry/conventions/attributes'; +import { MESSAGING_PROCESS_DURATION } from '@sentry/conventions/metrics'; import { metrics } from '@sentry/core'; import { toMessagingKey, toSentryAttributes } from './span'; import type { AttributeValue, Counter, Gauge, Histogram, Meter, MetricOptions } from './types'; @@ -91,7 +92,7 @@ class SentryBullMQGauge implements Gauge { */ class SentryBullMQProcessDuration implements Histogram { public record(value: number, attributes?: Record): void { - metrics.distribution('messaging.process.duration', value / 1000, { + metrics.distribution(MESSAGING_PROCESS_DURATION, value / 1000, { unit: 'second', attributes: { ...toMetricAttributes(attributes), diff --git a/packages/node/test/integrations/fs.test.ts b/packages/node/test/integrations/fs.test.ts new file mode 100644 index 000000000000..dfe107feac42 --- /dev/null +++ b/packages/node/test/integrations/fs.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it, vi } from 'vitest'; + +const { syncBuiltinESMExports } = vi.hoisted(() => ({ syncBuiltinESMExports: vi.fn() })); + +// Deno before 2.8.2 has `syncBuiltinESMExports` only on the default export of `node:module`, so a named import of it fails to link. +vi.mock('module', () => ({ default: { syncBuiltinESMExports } })); +vi.mock('fs', () => ({ default: { promises: {}, realpath: {}, realpathSync: {} } })); + +describe('enableFsInstrumentation', () => { + it('syncs the builtin ESM exports when `module` has no named `syncBuiltinESMExports` export', async () => { + const { enableFsInstrumentation } = await import('../../src/integrations/fs/vendored/instrumentation'); + + enableFsInstrumentation(); + + expect(syncBuiltinESMExports).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/node/test/integrations/tracing/bullmq.test.ts b/packages/node/test/integrations/tracing/bullmq.test.ts index a4fec420645f..fc0e62d6ae5b 100644 --- a/packages/node/test/integrations/tracing/bullmq.test.ts +++ b/packages/node/test/integrations/tracing/bullmq.test.ts @@ -1,4 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + MESSAGING_BULLMQ_JOBS_COMPLETED, + MESSAGING_BULLMQ_JOBS_FAILED, + MESSAGING_BULLMQ_QUEUE_JOBS, + MESSAGING_PROCESS_DURATION, +} from '@sentry/conventions/metrics'; import * as SentryCore from '@sentry/core'; import { BullMQTelemetry } from '../../../src/integrations/tracing/bullmq'; @@ -573,7 +579,7 @@ describe('SentryBullMQMeter', () => { counter.add(5, { 'queue.name': 'emails' }); - expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 5, { + expect(SentryCore.metrics.count).toHaveBeenCalledWith(MESSAGING_BULLMQ_JOBS_COMPLETED, 5, { unit: 'none', attributes: { 'queue.name': 'emails' }, }); @@ -585,7 +591,7 @@ describe('SentryBullMQMeter', () => { counter.add(1); - expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.failed', 1, { + expect(SentryCore.metrics.count).toHaveBeenCalledWith(MESSAGING_BULLMQ_JOBS_FAILED, 1, { unit: undefined, attributes: undefined, }); @@ -599,7 +605,7 @@ describe('SentryBullMQMeter', () => { histogram.record(1500, { 'bullmq.queue.name': 'notifications' }); - expect(SentryCore.metrics.distribution).toHaveBeenCalledWith('messaging.process.duration', 1.5, { + expect(SentryCore.metrics.distribution).toHaveBeenCalledWith(MESSAGING_PROCESS_DURATION, 1.5, { unit: 'second', attributes: { 'messaging.destination.name': 'notifications', @@ -629,7 +635,7 @@ describe('SentryBullMQMeter', () => { gauge.record(37, { 'queue.name': 'reports' }); - expect(SentryCore.metrics.gauge).toHaveBeenCalledWith('messaging.bullmq.queue.jobs', 37, { + expect(SentryCore.metrics.gauge).toHaveBeenCalledWith(MESSAGING_BULLMQ_QUEUE_JOBS, 37, { unit: 'none', attributes: { 'queue.name': 'reports' }, }); @@ -643,7 +649,7 @@ describe('SentryBullMQMeter', () => { counter.add(1, { 'bullmq.queue.name': 'emails', 'bullmq.job.name': 'welcome-email' }); - expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 1, { + expect(SentryCore.metrics.count).toHaveBeenCalledWith(MESSAGING_BULLMQ_JOBS_COMPLETED, 1, { unit: undefined, attributes: { 'messaging.bullmq.job.name': 'welcome-email', @@ -663,7 +669,7 @@ describe('SentryBullMQMeter', () => { enabled: true, }); - expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 1, { + expect(SentryCore.metrics.count).toHaveBeenCalledWith(MESSAGING_BULLMQ_JOBS_COMPLETED, 1, { unit: undefined, attributes: { 'queue.name': 'emails', @@ -679,7 +685,7 @@ describe('SentryBullMQMeter', () => { counter.add(1, { tags: ['a', 'b'] }); - expect(SentryCore.metrics.count).toHaveBeenCalledWith('messaging.bullmq.jobs.completed', 1, { + expect(SentryCore.metrics.count).toHaveBeenCalledWith(MESSAGING_BULLMQ_JOBS_COMPLETED, 1, { unit: undefined, attributes: {}, }); diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 8aa46b054a07..13599ef3bbf3 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -56,7 +56,7 @@ "@nuxt/kit": "^4.6.0", "@sentry/browser": "11.11.0", "@sentry/cloudflare": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/bundler-plugins": "11.11.0", @@ -76,7 +76,7 @@ "nuxi": "^3.25.1", "nuxt": "3.21.11", "unstorage": "^1.17.5", - "vite": "^8.3.5", + "vite": "^8.3.6", "vue": "^3.5.41" }, "scripts": { @@ -88,9 +88,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-nuxt-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-nuxt-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/nuxt/src/common/tunnel.ts b/packages/nuxt/src/common/tunnel.ts index 9dd578516af2..ba8b24c18115 100644 --- a/packages/nuxt/src/common/tunnel.ts +++ b/packages/nuxt/src/common/tunnel.ts @@ -1,5 +1,5 @@ import type { Options } from '@sentry/core'; -import { _INTERNAL_applyTunnelDefaults as applyTunnelDefaults, GLOBAL_OBJ } from '@sentry/core'; +import { _INTERNAL_applyBrowserTunnelDefaults as applyBrowserTunnelDefaults, GLOBAL_OBJ } from '@sentry/core'; /** Set by the generated client config plugin to the app's runtime `app.baseURL`. */ export const NUXT_TUNNEL_BASE_PATH_GLOBAL = '__sentry_nuxt_base_path'; @@ -11,5 +11,5 @@ export const NUXT_TUNNEL_BASE_PATH_GLOBAL = '__sentry_nuxt_base_path'; */ export function applyNuxtTunnelBasePath(options: Pick): void { const basePath = (GLOBAL_OBJ as { [NUXT_TUNNEL_BASE_PATH_GLOBAL]?: string })[NUXT_TUNNEL_BASE_PATH_GLOBAL]; - options.tunnel = applyTunnelDefaults(options.tunnel, { basePath }); + options.tunnel = applyBrowserTunnelDefaults(options.tunnel, { basePath }); } diff --git a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts index 515b5350880c..5a448a8665ca 100644 --- a/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts +++ b/packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts @@ -46,7 +46,7 @@ export function updateRouteBeforeResponse(event: EventWithMatchedRoute): void { if (isObjectLike(params)) { Object.entries(params).forEach(([key, value]) => { - // Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey + // Based on this convention: https://conventions.sentry.dev/attributes/url/#url-path-parameter-key rootSpan.setAttributes({ [`url.path.parameter.${key}`]: String(value), [`params.${key}`]: String(value), diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index 1893de6a57b1..45dfa9a87a55 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,7 +1,9 @@ +import type { ExecutionContext } from '@cloudflare/workers-types'; import type { CloudflareOptions } from '@sentry/cloudflare'; import { getDefaultIntegrations, setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; import { wrapRequestHandler } from '@sentry/cloudflare/request'; import { debug, getDefaultIsolationScope, getIsolationScope, getTraceData } from '@sentry/core'; +import { _INTERNAL_isServedTunnelRequest as isServedTunnelRequest } from '@sentry/core/server'; import type { H3Event } from 'h3'; import type { NitroApp, NitroAppPlugin } from 'nitropack'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -47,6 +49,40 @@ export const sentryCloudflareNitroPlugin = (nitroApp: NitroApp): void => { const traceDataMap = new WeakMap>(); + // Nitro v3 (Nuxt 5) has no `localFetch`. It hands the Worker's own request to `fetch`, with `ctx` on it. + const nitroV3App = nitroApp as unknown as { localFetch?: unknown; fetch?: (request: Request) => Promise }; + const appFetch = nitroV3App.fetch; + if (typeof nitroV3App.localFetch !== 'function' && typeof appFetch === 'function') { + nitroV3App.fetch = request => { + const runtime = (request as Request & { runtime?: { cloudflare?: { context?: ExecutionContext } } }).runtime; + // Internal `$fetch` calls go through `fetch` too, without the Worker's context. They run inside the request + // that made them, so they aren't wrapped again. + if (!runtime?.cloudflare?.context) { + return appFetch(request); + } + + setAsyncLocalStorageAsyncContextStrategy(); + + const cloudflareOptions = typeof optionsOrFn === 'function' ? optionsOrFn(nitroApp) : optionsOrFn; + + return wrapRequestHandler( + { + options: { + defaultIntegrations: getDefaultIntegrations(cloudflareOptions), + _extendsDefaultIntegrations: cloudflareOptions.defaultIntegrations === undefined, + ...cloudflareOptions, + }, + request, + context: runtime.cloudflare.context, + }, + () => appFetch(request), + ); + }; + nitroApp.hooks.hook('error', sentryCaptureErrorHook); + + return; + } + nitroApp.localFetch = new Proxy(nitroApp.localFetch, { async apply(handlerTarget, handlerThisArg, handlerArgs: [string, unknown]) { setAsyncLocalStorageAsyncContextStrategy(); @@ -64,6 +100,9 @@ export const sentryCloudflareNitroPlugin = const request = new Request(url, { method: event.method, headers: event.headers, + // Nitro read the body before calling `localFetch`. Only the managed tunnel answers from this request, so + // only its requests get the body, and other requests' bodies don't end up on events. + body: isServedTunnelRequest({ method: event.method, url: pathname }) ? event.body : undefined, // @ts-expect-error - 'cf' is a valid property in the RequestInit type for Cloudflare cf: getCfProperties(event), }); diff --git a/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts index 89b1a04e8958..51aed67bffed 100644 --- a/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts +++ b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts @@ -1,33 +1,12 @@ -import { - _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, -} from '@sentry/core/server'; -import { eventHandler, toWebRequest } from 'h3'; +import { _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server'; import type { NitroAppPlugin } from 'nitropack'; import { useRuntimeConfig } from 'nitropack/runtime'; /** - * Answers managed tunnel requests on Nuxt v3 and v4 (Nitro v2), from the first layer of the h3 app. That runs - * before route rules and the app's `server/middleware`. + * Serves the managed tunnel under Nuxt's `app.baseURL` on Nuxt v3 and v4 (Nitro v2). The runtime answers the + * requests. */ -export default (nitroApp => { - // The first layer sees the path before Nitro strips `app.baseURL`, so the tunnel is served under it. +export default (() => { // `useRuntimeConfig` also picks up a `NUXT_APP_BASE_URL` set at runtime, the same value the browser gets. - setManagedTunnelDefaults({ basePath: useRuntimeConfig().app?.baseURL }); - - // Not the `request` hook: h3 still runs the first layer after a hook answered, and with a `headers` route rule - // that layer throws for writing headers on the sent response. - nitroApp.h3App.stack.unshift({ - route: '', - handler: eventHandler(event => { - // Checked on the method and path first, so no other request's body gets wrapped in a web `Request`. - if (!isServedTunnelRequest({ method: event.method, url: event.path })) { - return undefined; - } - - // A returned `Response` is sent and ends the request, while `undefined` hands it to the next layer. - return handleServedTunnelRequest(toWebRequest(event)); - }), - }); + setTunnelDefaults({ basePath: useRuntimeConfig().app?.baseURL }); }) satisfies NitroAppPlugin; diff --git a/packages/nuxt/src/runtime/plugins/tunnel.server.ts b/packages/nuxt/src/runtime/plugins/tunnel.server.ts index ae4b451edc32..611466e662df 100644 --- a/packages/nuxt/src/runtime/plugins/tunnel.server.ts +++ b/packages/nuxt/src/runtime/plugins/tunnel.server.ts @@ -1,20 +1,11 @@ -import { - _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, -} from '@sentry/core/server'; +import { _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server'; import { useRuntimeConfig } from 'nitro/runtime-config'; -import type { NitroAppPlugin, NitroApp } from 'nitro/types'; +import type { NitroAppPlugin } from 'nitro/types'; /** - * Answers managed tunnel requests on Nuxt v5+ (Nitro v3+). + * Serves the managed tunnel under Nuxt's `app.baseURL` on Nuxt v5+ (Nitro v3+). The runtime answers the requests. */ -export default ((nitroApp: NitroApp) => { - // Nitro runs the app's `server/middleware` before anything a module registers, so an auth middleware there would - // block the tunnel. Every preset hands requests to `nitroApp.fetch` after the plugins ran, so wrapping it goes - // first, before route rules and middleware. - // Requests still carry Nuxt's `app.baseURL` here, so the tunnel is served under it. `useRuntimeConfig` also picks - // up a `NUXT_APP_BASE_URL` set at runtime. - setManagedTunnelDefaults({ basePath: (useRuntimeConfig() as { app?: { baseURL?: string } }).app?.baseURL }); - const appFetch = nitroApp.fetch; - nitroApp.fetch = request => handleServedTunnelRequest(request) ?? appFetch(request); +export default (() => { + // `useRuntimeConfig` also picks up a `NUXT_APP_BASE_URL` set at runtime, the same value the browser gets. + setTunnelDefaults({ basePath: (useRuntimeConfig() as { app?: { baseURL?: string } }).app?.baseURL }); }) satisfies NitroAppPlugin; diff --git a/packages/nuxt/src/runtime/utils/event-type-check.ts b/packages/nuxt/src/runtime/utils/event-type-check.ts index 1da0d8b2677a..14447c072c64 100644 --- a/packages/nuxt/src/runtime/utils/event-type-check.ts +++ b/packages/nuxt/src/runtime/utils/event-type-check.ts @@ -5,6 +5,8 @@ interface EventBase { host: string; method: string; headers: Record; + /** The request body Nitro already read, e.g. a `Buffer`. */ + body?: BodyInit | null; } interface MinimalCloudflareProps { diff --git a/packages/nuxt/src/vite/nitroServer.ts b/packages/nuxt/src/vite/nitroServer.ts index 0a7f358f1182..7401a6c1596d 100644 --- a/packages/nuxt/src/vite/nitroServer.ts +++ b/packages/nuxt/src/vite/nitroServer.ts @@ -120,7 +120,7 @@ function setupDeprecatedInjectMode( } /** - * Adds the plugin that answers the managed tunnel. The Cloudflare setup has no server config file, so it skips + * Adds the plugin that sets the managed tunnel's base path. The Cloudflare setup has no server config file, so it skips * `setupNitroServer` and adds this on its own. */ export function addTunnelPlugin(serverApi: ServerApi): void { diff --git a/packages/nuxt/src/vite/sourceMapDeletion.ts b/packages/nuxt/src/vite/sourceMapDeletion.ts index 7f9784e3bd37..874381d66b36 100644 --- a/packages/nuxt/src/vite/sourceMapDeletion.ts +++ b/packages/nuxt/src/vite/sourceMapDeletion.ts @@ -48,6 +48,7 @@ export function withoutSourceMapDeletion(options: Options): Options { sourcemaps: { ...options.sourcemaps, filesToDeleteAfterUpload: undefined, + deleteSourcemapsAfterUpload: false, }, }; } diff --git a/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts b/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts new file mode 100644 index 000000000000..e5d2a42c7319 --- /dev/null +++ b/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts @@ -0,0 +1,45 @@ +import { createApp, createRouter, defineWebSocketHandler } from 'h3'; +import type { NitroApp, NitroAppPlugin } from 'nitropack'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import legacyTunnelPlugin from '../../../src/runtime/plugins/tunnel-legacy.server'; + +const { setTunnelDefaults, useRuntimeConfig } = vi.hoisted(() => ({ + setTunnelDefaults: vi.fn(), + useRuntimeConfig: vi.fn(), +})); + +vi.mock('@sentry/core/server', () => ({ + _INTERNAL_setTunnelDefaults: setTunnelDefaults, +})); + +vi.mock('nitropack/runtime', () => ({ useRuntimeConfig })); + +const tunnelPlugin: NitroAppPlugin = legacyTunnelPlugin; + +describe('tunnel-legacy.server', () => { + beforeEach(() => { + vi.resetAllMocks(); + useRuntimeConfig.mockReturnValue({ app: { baseURL: '/base/' } }); + }); + + it("serves the tunnel under the app's base URL", () => { + tunnelPlugin({ h3App: createApp() } as unknown as NitroApp); + + expect(setTunnelDefaults).toHaveBeenCalledWith({ basePath: '/base/' }); + }); + + it("keeps the app's WebSocket handlers resolvable", async () => { + const app = createApp(); + const router = createRouter(); + const upgrade = vi.fn(); + router.use('/ws/:code', defineWebSocketHandler({ upgrade })); + // Nitro mounts its router on the app base URL + app.use('/', router.handler); + + tunnelPlugin({ h3App: app } as unknown as NitroApp); + + const hooks = await app.websocket.resolve!(new Request('http://localhost/ws/abc') as never); + + expect(hooks.upgrade).toBe(upgrade); + }); +}); diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index b6bc0b5109ba..ac10b8a871df 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -247,6 +247,7 @@ describe('setupSourceMaps hooks', () => { assets: undefined, ignore: undefined, filesToDeleteAfterUpload: undefined, + deleteSourcemapsAfterUpload: false, rewriteSources: expect.any(Function), }, }); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 48fb05517a03..fd3bf2536f2f 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0" }, "peerDependencies": { @@ -45,9 +45,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-opentelemetry-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-opentelemetry-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 30caa5cf2ac3..eaa930204ea4 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -44,13 +44,13 @@ "build:dev": "pnpm clean && pnpm run build", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:watch": "run-p build:transpile:watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "test:bundle": "node test-binaries.esbuild.js", "test": "vitest run", "test:watch": "vitest --watch" }, "dependencies": { - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/node-cpu-profiler": "^2.4.4", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index c62c80ef8018..9c69f5041ddc 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -57,14 +57,14 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", "@sentry/bundler-plugins": "11.11.0", - "sentry": "^0.44.0" + "sentry": "^0.49.0" }, "devDependencies": { "@react-router/dev": "^7.18.4", @@ -75,7 +75,7 @@ "@types/react": "17.0.3", "react": "^18.3.1", "react-router": "^7.18.4", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "peerDependencies": { "@react-router/node": ">=7.15.0 || ^8.x", @@ -91,9 +91,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-react-router-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-react-router-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/react-router/src/client/sdk.ts b/packages/react-router/src/client/sdk.ts index ad7b9bb94941..9b8102d84bf0 100644 --- a/packages/react-router/src/client/sdk.ts +++ b/packages/react-router/src/client/sdk.ts @@ -3,7 +3,7 @@ import { init as browserInit } from '@sentry/browser'; import type { Client } from '@sentry/core'; import { applySdkMetadata, - _INTERNAL_applyTunnelDefaults as applyTunnelDefaults, + _INTERNAL_applyBrowserTunnelDefaults as applyBrowserTunnelDefaults, consoleSandbox, GLOBAL_OBJ, setTag, @@ -37,7 +37,7 @@ export function init(options: BrowserOptions): Client | undefined { } // The managed tunnel is served under the app's `basename`, like every other route. A `basePath` the user set wins. - options.tunnel = applyTunnelDefaults(options.tunnel, { + options.tunnel = applyBrowserTunnelDefaults(options.tunnel, { basePath: GLOBAL_OBJ_WITH_ROUTER_CONTEXT.__reactRouterContext?.basename, }); diff --git a/packages/react-router/src/cloudflare/index.ts b/packages/react-router/src/cloudflare/index.ts index 481d2169cf07..f28bb1d4924e 100644 --- a/packages/react-router/src/cloudflare/index.ts +++ b/packages/react-router/src/cloudflare/index.ts @@ -1,7 +1,7 @@ export * from '../client'; export { wrapSentryHandleRequest } from '../server/wrapSentryHandleRequest'; -export { createSentryTunnelRoutes } from '../server/tunnelRoute'; +export { setTunnelBasePath as _INTERNAL_setTunnelBasePath } from '../server/tunnelRoute'; export { createSentryServerInstrumentation, type CreateSentryServerInstrumentationOptions, diff --git a/packages/react-router/src/server/index.ts b/packages/react-router/src/server/index.ts index 71d37c6ca114..06f1e335410e 100644 --- a/packages/react-router/src/server/index.ts +++ b/packages/react-router/src/server/index.ts @@ -9,7 +9,7 @@ export { createSentryHandleRequest, type SentryHandleRequestOptions } from './cr export type { SentryWebStreamHandleRequestOptions } from './handleRequestTypes'; export { createSentryHandleError, type SentryHandleErrorOptions } from './createSentryHandleError'; export { getMetaTagTransformer } from './getMetaTagTransformer'; -export { createSentryTunnelRoutes } from './tunnelRoute'; +export { setTunnelBasePath as _INTERNAL_setTunnelBasePath } from './tunnelRoute'; // React Router instrumentation API support export { diff --git a/packages/react-router/src/server/tunnelRoute.ts b/packages/react-router/src/server/tunnelRoute.ts index 18b03a6d2c13..1d10aa356f19 100644 --- a/packages/react-router/src/server/tunnelRoute.ts +++ b/packages/react-router/src/server/tunnelRoute.ts @@ -1,70 +1,12 @@ -import { debug, getClient } from '@sentry/core'; -import { - _INTERNAL_getTunnelPaths as getTunnelPaths, - _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, -} from '@sentry/core/server'; -import { DEBUG_BUILD } from '../common/debug-build'; - -// Never matches a request, for when the managed tunnel is off. -const DISABLED_TUNNEL_PATH = '/__sentry-tunnel-disabled__'; +import { _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server'; /** - * The resource routes that serve the managed tunnel, one per tunnel path. `sentryReactRouter()` adds them to the - * server build, since React Router has no request hook to answer from and a route matches only one path. Not meant - * to be used directly. + * Serves the managed tunnel under the app's `basename`, which React Router leaves in the request URL. + * `sentryReactRouter()` calls it from the server build. * - * @param basename The app's `basename`, which React Router matches the routes under but leaves in the request URL. + * @param basename The app's `basename`. */ -export function createSentryTunnelRoutes(basename?: string): Record> { +export function setTunnelBasePath(basename?: string): void { // Runs when the server build loads. Clients created later, e.g. per request on Cloudflare, start with the basename. - setManagedTunnelDefaults({ basePath: basename }); - - // `init` runs from `--import` before the server build loads, so the client and its tunnel paths are usually known. - if (!getClient()) { - return { 'sentry-tunnel': makeTunnelRoute('sentry-tunnel', getLazyTunnelPath) }; - } - - return Object.fromEntries( - getTunnelPaths().map((path, index) => { - const id = `sentry-tunnel-${index}`; - - return [id, makeTunnelRoute(id, () => path)]; - }), - ); -} - -/** - * The path of the single route added when `init` runs after the server build loads, e.g. per request on Cloudflare. - * React Router reads it on its first request and keeps it until the server restarts. - */ -function getLazyTunnelPath(): string { - if (!getClient()) { - DEBUG_BUILD && debug.warn('The managed tunnel stays off until restart: a request came in before `Sentry.init`.'); - return DISABLED_TUNNEL_PATH; - } - - const [path, ...otherPaths] = getTunnelPaths(); - if (otherPaths.length) { - DEBUG_BUILD && debug.warn(`Sentry.init ran after the server build loaded, so only ${path} is served.`); - } - - return path ?? DISABLED_TUNNEL_PATH; -} - -function makeTunnelRoute(id: string, getPath: () => string): Record { - return { - id, - parentId: undefined, - index: undefined, - caseSensitive: undefined, - get path(): string { - return getPath(); - }, - module: { - action: ({ request }: { request: Request }) => - handleServedTunnelRequest(request) ?? new Response(null, { status: 404 }), - loader: () => new Response(null, { status: 405, headers: { Allow: 'POST' } }), - }, - }; + setTunnelDefaults({ basePath: basename }); } diff --git a/packages/react-router/src/vite/makeTunnelBasePathPlugin.ts b/packages/react-router/src/vite/makeTunnelBasePathPlugin.ts new file mode 100644 index 000000000000..d74fcf4b7fe9 --- /dev/null +++ b/packages/react-router/src/vite/makeTunnelBasePathPlugin.ts @@ -0,0 +1,28 @@ +import type { Plugin } from 'vite'; + +const SERVER_BUILD_MODULE_ID = 'virtual:react-router/server-build'; + +/** + * Passes the app's `basename` from the server build to the managed tunnel, which serves under it. Only applies when + * `serveTunnelIntegration` is set up. + */ +export function makeTunnelBasePathPlugin(): Plugin { + return { + name: 'sentry-react-router-tunnel-base-path', + enforce: 'post', + + transform(code, id, options) { + if (!options?.ssr || !id.includes(SERVER_BUILD_MODULE_ID)) { + return null; + } + + return { + code: `${code} +import { _INTERNAL_setTunnelBasePath as __sentrySetTunnelBasePath } from '@sentry/react-router'; +__sentrySetTunnelBasePath(typeof basename === 'string' ? basename : undefined); +`, + map: null, + }; + }, + }; +} diff --git a/packages/react-router/src/vite/makeTunnelRoutePlugin.ts b/packages/react-router/src/vite/makeTunnelRoutePlugin.ts deleted file mode 100644 index 5d2c12cb244c..000000000000 --- a/packages/react-router/src/vite/makeTunnelRoutePlugin.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Plugin } from 'vite'; - -const SERVER_BUILD_MODULE_ID = 'virtual:react-router/server-build'; - -/** - * Adds the managed tunnel routes to the server build. They only serve requests when `serveTunnelIntegration` is set - * up. The server build exports the app's `basename`, which the route needs to match the request URL. - */ -export function makeTunnelRoutePlugin(): Plugin { - return { - name: 'sentry-react-router-tunnel-route', - enforce: 'post', - - transform(code, id, options) { - if (!options?.ssr || !id.includes(SERVER_BUILD_MODULE_ID)) { - return null; - } - - return { - code: `${code} -import { createSentryTunnelRoutes as __sentryCreateTunnelRoutes } from '@sentry/react-router'; -if (typeof routes === 'object' && routes) { - Object.assign(routes, __sentryCreateTunnelRoutes(typeof basename === 'string' ? basename : undefined)); -} -`, - map: null, - }; - }, - }; -} diff --git a/packages/react-router/src/vite/plugin.ts b/packages/react-router/src/vite/plugin.ts index 2992b9a5bbc0..ced7b61048ec 100644 --- a/packages/react-router/src/vite/plugin.ts +++ b/packages/react-router/src/vite/plugin.ts @@ -4,7 +4,7 @@ import { makeConfigInjectorPlugin } from './makeConfigInjectorPlugin'; import { makeCustomSentryVitePlugins } from './makeCustomSentryVitePlugins'; import { makeEnableSourceMapsPlugin } from './makeEnableSourceMapsPlugin'; import { makeServerBuildCapturePlugin } from './makeServerBuildCapturePlugin'; -import { makeTunnelRoutePlugin } from './makeTunnelRoutePlugin'; +import { makeTunnelBasePathPlugin } from './makeTunnelBasePathPlugin'; import type { SentryReactRouterBuildOptions } from './types'; /** @@ -22,7 +22,7 @@ export async function sentryReactRouter( plugins.push(makeConfigInjectorPlugin(options)); plugins.push(makeServerBuildCapturePlugin()); - plugins.push(makeTunnelRoutePlugin()); + plugins.push(makeTunnelBasePathPlugin()); if (process.env.NODE_ENV !== 'development' && viteConfig.command === 'build' && viteConfig.mode !== 'development') { plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation })); diff --git a/packages/react-router/test/server/tunnelRoute.test.ts b/packages/react-router/test/server/tunnelRoute.test.ts deleted file mode 100644 index 38b2a7c7900b..000000000000 --- a/packages/react-router/test/server/tunnelRoute.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Client } from '@sentry/core'; -import { createEnvelope, getCurrentScope, getTunnelPath, makeDsn, serializeEnvelope } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { createSentryTunnelRoutes } from '../../src/server/tunnelRoute'; - -const DSN = 'https://public@o1.ingest.sentry.io/1337'; - -type TunnelRoute = { - path: string; - parentId: undefined; - module: { action: (args: { request: Request }) => Promise | Response; loader: () => Response }; -}; - -function firstRoute(basename?: string): TunnelRoute { - return Object.values(createSentryTunnelRoutes(basename))[0] as TunnelRoute; -} - -/** Sets a client, with `serveTunnelIntegration` set up when `managedTunnel` is given. */ -function setClient(managedTunnel: object | undefined): void { - const options = { dsn: DSN }; - const client = { getDsn: () => makeDsn(DSN), getOptions: () => options } as unknown as Client; - getCurrentScope().setClient(client); - if (managedTunnel) { - serveTunnelIntegration(managedTunnel).setup!(client); - } -} - -const ENVELOPE = serializeEnvelope(createEnvelope({ dsn: DSN }, [])) as string; - -describe('createSentryTunnelRoutes', () => { - afterEach(() => { - getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); - vi.unstubAllGlobals(); - }); - - it('is a top-level route at the tunnel path when `serveTunnelIntegration` is set up', () => { - setClient({}); - const route = firstRoute(); - - expect(route.parentId).toBeUndefined(); - expect(route.path).toBe(getTunnelPath(DSN)); - }); - - it('adds no route when the tunnel is off, and a path that never matches when Sentry is not initialized yet', () => { - const route = firstRoute(); - const pathWithoutClient = route.path; - setClient(undefined); - - expect(pathWithoutClient).toBe('/__sentry-tunnel-disabled__'); - expect(route.path).toBe('/__sentry-tunnel-disabled__'); - expect(createSentryTunnelRoutes()).toEqual({}); - }); - - it('forwards tunnel POSTs and rejects other methods', async () => { - setClient({}); - vi.stubGlobal( - 'fetch', - vi.fn(() => Promise.resolve(new Response('forwarded'))), - ); - const route = firstRoute(); - const request = new Request(`http://localhost${getTunnelPath(DSN)}`, { - method: 'POST', - body: ENVELOPE, - headers: { 'content-length': String(ENVELOPE.length) }, - }); - - expect(await (await route.module.action({ request })).text()).toBe('forwarded'); - expect(route.module.loader().status).toBe(405); - }); - - it('serves the tunnel under `basename`, which React Router leaves in the request URL', async () => { - setClient({}); - vi.stubGlobal( - 'fetch', - vi.fn(() => Promise.resolve(new Response('forwarded'))), - ); - const route = firstRoute('/app/'); - const request = new Request(`http://localhost/app${getTunnelPath(DSN)}`, { - method: 'POST', - body: ENVELOPE, - headers: { 'content-length': String(ENVELOPE.length) }, - }); - - // React Router matches the route under `basename` itself, so the path stays relative to it. - expect(route.path).toBe(getTunnelPath(DSN)); - expect(await (await route.module.action({ request })).text()).toBe('forwarded'); - }); -}); diff --git a/packages/react-router/test/vite/makeTunnelBasePathPlugin.test.ts b/packages/react-router/test/vite/makeTunnelBasePathPlugin.test.ts new file mode 100644 index 000000000000..9925ca578ccc --- /dev/null +++ b/packages/react-router/test/vite/makeTunnelBasePathPlugin.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { makeTunnelBasePathPlugin } from '../../src/vite/makeTunnelBasePathPlugin'; + +type Transform = (code: string, id: string, options?: { ssr?: boolean }) => { code: string } | null; + +describe('makeTunnelBasePathPlugin', () => { + const transform = makeTunnelBasePathPlugin().transform as unknown as Transform; + + it('passes the basename of the SSR server build to the tunnel', () => { + const result = transform('export const basename = "/app";', '\0virtual:react-router/server-build', { ssr: true }); + + expect(result?.code).toContain("__sentrySetTunnelBasePath(typeof basename === 'string' ? basename : undefined);"); + expect(result?.code).toContain("from '@sentry/react-router'"); + }); + + it('leaves other modules and the client build alone', () => { + expect(transform('', '\0virtual:react-router/server-build', { ssr: false })).toBeNull(); + expect(transform('', '/app/root.tsx', { ssr: true })).toBeNull(); + }); +}); diff --git a/packages/react-router/test/vite/makeTunnelRoutePlugin.test.ts b/packages/react-router/test/vite/makeTunnelRoutePlugin.test.ts deleted file mode 100644 index d520315a07bc..000000000000 --- a/packages/react-router/test/vite/makeTunnelRoutePlugin.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { makeTunnelRoutePlugin } from '../../src/vite/makeTunnelRoutePlugin'; - -type Transform = (code: string, id: string, options?: { ssr?: boolean }) => { code: string } | null; - -describe('makeTunnelRoutePlugin', () => { - const transform = makeTunnelRoutePlugin().transform as unknown as Transform; - - it('adds the tunnel route to the SSR server build', () => { - const result = transform('export const routes = {};', '\0virtual:react-router/server-build', { ssr: true }); - - expect(result?.code).toContain( - " Object.assign(routes, __sentryCreateTunnelRoutes(typeof basename === 'string' ? basename : undefined));", - ); - expect(result?.code).toContain("from '@sentry/react-router'"); - }); - - it('only adds the route when the server build declares `routes`, so a build without it still loads', () => { - const result = transform('export const routes = {};', '\0virtual:react-router/server-build', { ssr: true }); - - expect(result?.code).toContain("if (typeof routes === 'object' && routes) {"); - }); - - it('leaves other modules and the client build alone', () => { - expect(transform('', '\0virtual:react-router/server-build', { ssr: false })).toBeNull(); - expect(transform('', '/app/root.tsx', { ssr: true })).toBeNull(); - }); -}); diff --git a/packages/react-router/test/vite/plugin.test.ts b/packages/react-router/test/vite/plugin.test.ts index 5c33619821df..b2c9774027d6 100644 --- a/packages/react-router/test/vite/plugin.test.ts +++ b/packages/react-router/test/vite/plugin.test.ts @@ -3,7 +3,7 @@ import { makeConfigInjectorPlugin } from '../../src/vite/makeConfigInjectorPlugi import { makeCustomSentryVitePlugins } from '../../src/vite/makeCustomSentryVitePlugins'; import { makeEnableSourceMapsPlugin } from '../../src/vite/makeEnableSourceMapsPlugin'; import { makeServerBuildCapturePlugin } from '../../src/vite/makeServerBuildCapturePlugin'; -import { makeTunnelRoutePlugin } from '../../src/vite/makeTunnelRoutePlugin'; +import { makeTunnelBasePathPlugin } from '../../src/vite/makeTunnelBasePathPlugin'; import { sentryReactRouter } from '../../src/vite/plugin'; vi.spyOn(console, 'log').mockImplementation(() => { @@ -17,7 +17,7 @@ vi.mock('../../src/vite/makeCustomSentryVitePlugins'); vi.mock('../../src/vite/makeEnableSourceMapsPlugin'); vi.mock('../../src/vite/makeConfigInjectorPlugin'); vi.mock('../../src/vite/makeServerBuildCapturePlugin'); -vi.mock('../../src/vite/makeTunnelRoutePlugin'); +vi.mock('../../src/vite/makeTunnelBasePathPlugin'); // Stub the orchestrion plugin so these stay pure wiring tests (no apm code transformer pulled in). // Mirror the real plugin's contract: `buildTimeInstrumentation: false` yields the inert variant. @@ -33,7 +33,7 @@ describe('sentryReactRouter', () => { const mockSourceMapsPlugin = { name: 'source-maps-plugin' }; const mockConfigInjectorPlugin = { name: 'sentry-config-injector' }; const mockServerBuildCapturePlugin = { name: 'sentry-react-router-server-build-capture' }; - const mockTunnelRoutePlugin = { name: 'sentry-react-router-tunnel-route' }; + const mockTunnelBasePathPlugin = { name: 'sentry-react-router-tunnel-base-path' }; const mockOrchestrionPlugin = { name: 'sentry-orchestrion-vite' }; beforeEach(() => { @@ -42,7 +42,7 @@ describe('sentryReactRouter', () => { vi.mocked(makeEnableSourceMapsPlugin).mockReturnValue(mockSourceMapsPlugin); vi.mocked(makeConfigInjectorPlugin).mockReturnValue(mockConfigInjectorPlugin); vi.mocked(makeServerBuildCapturePlugin).mockReturnValue(mockServerBuildCapturePlugin); - vi.mocked(makeTunnelRoutePlugin).mockReturnValue(mockTunnelRoutePlugin); + vi.mocked(makeTunnelBasePathPlugin).mockReturnValue(mockTunnelBasePathPlugin); }); afterEach(() => { @@ -55,7 +55,7 @@ describe('sentryReactRouter', () => { const result = await sentryReactRouter({}, { command: 'build', mode: 'production' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockTunnelRoutePlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockTunnelBasePathPlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); @@ -65,7 +65,7 @@ describe('sentryReactRouter', () => { it('should return config injector plugin when not in build mode', async () => { const result = await sentryReactRouter({}, { command: 'serve', mode: 'production' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockTunnelRoutePlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockTunnelBasePathPlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); }); @@ -73,7 +73,7 @@ describe('sentryReactRouter', () => { it('should return config injector plugin in development build mode', async () => { const result = await sentryReactRouter({}, { command: 'build', mode: 'development' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockTunnelRoutePlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockTunnelBasePathPlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); }); @@ -87,7 +87,7 @@ describe('sentryReactRouter', () => { expect(result).toEqual([ mockConfigInjectorPlugin, mockServerBuildCapturePlugin, - mockTunnelRoutePlugin, + mockTunnelBasePathPlugin, mockOrchestrionPlugin, mockSourceMapsPlugin, ...mockPlugins, diff --git a/packages/react/package.json b/packages/react/package.json index 9cb321d1ea4f..1acbd7615376 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,7 +59,7 @@ "dependencies": { "@sentry/browser": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "react": "17.x || 18.x || 19.x", @@ -75,13 +75,10 @@ "@testing-library/react": "^15.0.5", "@testing-library/react-hooks": "^7.0.2", "@types/history-4": "npm:@types/history@4.7.8", - "@types/history-5": "npm:@types/history@4.7.8", - "@types/node-fetch": "^2.6.11", "@types/react": "17.0.3", "@types/react-router-4": "npm:@types/react-router@4.0.25", "@types/react-router-5": "npm:@types/react-router@5.1.20", "history-4": "npm:history@4.6.0", - "history-5": "npm:history@4.9.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.18.4", @@ -99,9 +96,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.ts && madge --circular src/react-router.ts", - "clean": "rimraf build coverage sentry-react-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.ts src/react-router.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-react-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/remix/package.json b/packages/remix/package.json index c45f348f7171..7f02104fe54d 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,13 +78,13 @@ "@remix-run/router": "^1.23.4", "@sentry/browser": "11.11.0", "@sentry/bundler-plugins": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", - "sentry": "^0.44.0", + "sentry": "^0.49.0", "yargs": "^18.2.0" }, "devDependencies": { @@ -93,13 +93,12 @@ "@remix-run/route-pattern": "^1.0.0", "@remix-run/server-runtime": "^2.17.4", "@sentry-internal/rollup-utils": "11.11.0", - "@types/express": "^4.17.14", "@types/react": "17.0.3", "@types/semver": "^7.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", "semver": "^7.8.5", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "peerDependencies": { "@remix-run/node": "2.x", @@ -116,9 +115,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.server.ts && madge --circular src/index.client.ts", - "clean": "rimraf build coverage sentry-remix-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.server.ts src/index.client.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-remix-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/remix/src/v3/server/instrument.ts b/packages/remix/src/v3/server/instrument.ts index f63578e2f0f9..0094d56e7594 100644 --- a/packages/remix/src/v3/server/instrument.ts +++ b/packages/remix/src/v3/server/instrument.ts @@ -1,7 +1,6 @@ import * as diagnosticsChannel from 'node:diagnostics_channel'; import { createMultiMatcher } from 'remix/route-pattern/match'; import { consoleSandbox } from '@sentry/core'; -import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest } from '@sentry/core/server'; import type { OrchestrionChannelContext } from '@sentry/server-utils'; import { remixV3Channels } from '@sentry/server-utils/orchestrion/config'; @@ -59,7 +58,7 @@ function subscribeToCreateRouter(): void { } /** - * Report errors from any fetch handler, router or not, and answer managed tunnel requests before it runs. + * Report errors from any fetch handler, router or not. * The middleware only sees failures inside a router; this is the only hook that covers a plain handler. * No abort guard is needed, because the listener drops aborted requests before calling `onError`. */ @@ -72,7 +71,6 @@ function subscribeToCreateRequestListener(): void { start(data) { try { injectOnError(ensureOptions(data.arguments, 1)); - wrapFetchHandler(data.arguments); } catch { // Ignored on purpose. } @@ -84,21 +82,6 @@ function subscribeToCreateRequestListener(): void { }); } -/** - * Answers managed tunnel requests before the app's fetch handler, so code the app runs in front of its router never - * sees them. The router middleware still answers them for apps served some other way. - */ -function wrapFetchHandler(args: unknown[]): void { - const handler = args[0]; - if (typeof handler !== 'function') { - return; - } - - args[0] = function sentryFetchHandler(this: unknown, request: Request, ...rest: unknown[]): unknown { - return handleServedTunnelRequest(request) ?? handler.call(this, request, ...rest); - }; -} - function injectOnError(raw: Record | undefined): void { if (!raw || markInjected(raw)) { return; diff --git a/packages/remix/test/v3/instrument.test.ts b/packages/remix/test/v3/instrument.test.ts index 85133e8b3ecd..cf22efc91609 100644 --- a/packages/remix/test/v3/instrument.test.ts +++ b/packages/remix/test/v3/instrument.test.ts @@ -144,33 +144,3 @@ describe('the createRequestListener error hook', () => { expect(args[1]).toEqual(expect.objectContaining({ onError: expect.any(Function) })); }); }); - -describe('the createRequestListener tunnel', () => { - beforeAll(() => { - instrumentRemixV3(); - }); - - it("answers a managed tunnel request without calling the app's handler", async () => { - const appHandler = vi.fn(() => new Response('app')); - const args: unknown[] = [appHandler]; - - publishCreateRequestListener(args); - - expect(await (args[0] as (request: Request) => unknown)(new Request(TUNNEL_URL, { method: 'POST' }))).toBe( - tunnelResponse, - ); - expect(appHandler).not.toHaveBeenCalled(); - }); - - it("hands every other request to the app's handler", () => { - const appResponse = new Response('app'); - const appHandler = vi.fn(() => appResponse); - const args: unknown[] = [appHandler]; - const request = new Request('http://localhost/page'); - - publishCreateRequestListener(args); - - expect((args[0] as (request: Request, client: unknown) => unknown)(request, 'client')).toBe(appResponse); - expect(appHandler).toHaveBeenCalledWith(request, 'client'); - }); -}); diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index df3f45a26c59..7c858cae1935 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -33,9 +33,9 @@ "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "pnpm run build:transpile --watch", "build:bundle:watch": "pnpm run build:bundle --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build sentry-replay-*.tgz", + "clean": "node ../../scripts/rm.mjs build sentry-replay-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", @@ -59,7 +59,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@sentry-internal/rollup-utils": "11.11.0", - "@sentry/rrweb": "2.45.0" + "@sentry/rrweb": "2.46.0" }, "dependencies": { "@sentry/replay": "11.11.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index d37425f6c4a9..a6e838dcca40 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -47,9 +47,9 @@ "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "pnpm run build:transpile --watch", "build:bundle:watch": "pnpm run build:bundle --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build sentry-replay-*.tgz", + "clean": "node ../../scripts/rm.mjs build sentry-replay-*.tgz", "format": "oxfmt \"src/**/*.ts\" \"test/**/*.ts\" --write", "format:check": "oxfmt \"src/**/*.ts\" \"test/**/*.ts\" --check", "lint": "oxlint . --type-aware", @@ -71,12 +71,11 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@babel/core": "^7.29.6", "@sentry-internal/replay-worker": "11.11.0", "@sentry-internal/rollup-utils": "11.11.0", - "@sentry/conventions": "^0.30.0", - "@sentry/rrweb": "2.45.0", - "@sentry/rrweb-snapshot": "2.45.0", + "@sentry/conventions": "^0.32.0", + "@sentry/rrweb": "2.46.0", + "@sentry/rrweb-snapshot": "2.46.0", "fflate": "0.8.3", "jsdom-worker": "^0.3.0" }, diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 5177ecc8c46d..17f01443b979 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -36,7 +36,7 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "pnpm run build:transpile --watch", - "clean": "rimraf build", + "clean": "node ../../scripts/rm.mjs build", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index d10f6573cdba..9f6dbc283144 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -41,8 +41,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-rollup-plugin-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-rollup-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", "test": "vitest run", diff --git a/packages/server-runtime-injection/package.json b/packages/server-runtime-injection/package.json index 059ef4239f8c..2dbe809e1279 100644 --- a/packages/server-runtime-injection/package.json +++ b/packages/server-runtime-injection/package.json @@ -65,8 +65,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-server-runtime-injection-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-server-runtime-injection-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 2490450b3313..5f952d76c334 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -102,7 +102,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0" }, "devDependencies": { @@ -111,7 +111,7 @@ "@sentry-internal/rollup-utils": "11.11.0", "@types/node": "^20.19.0", "meriyah": "^6.1.4", - "vite": "^8.3.5", + "vite": "^8.3.6", "webpack": "^5.112.2" }, "scripts": { @@ -122,8 +122,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-server-utils-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-server-utils-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/server-utils/src/ai/anthropic-ai/streaming.ts b/packages/server-utils/src/ai/anthropic-ai/streaming.ts index 1c3ed58674d6..1adc3413722d 100644 --- a/packages/server-utils/src/ai/anthropic-ai/streaming.ts +++ b/packages/server-utils/src/ai/anthropic-ai/streaming.ts @@ -247,6 +247,9 @@ export async function* instrumentAsyncIterableStream( processEvent(event, state, recordOutputs, span); yield event; } + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + throw error; } finally { endStreamSpan(span, state, recordOutputs); } @@ -296,7 +299,7 @@ export function instrumentMessageStream if (span.isRecording()) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - span.end(); + endStreamSpan(span, state, recordOutputs); } }); diff --git a/packages/server-utils/src/ai/anthropic-ai/tool-runner.ts b/packages/server-utils/src/ai/anthropic-ai/tool-runner.ts index b15a62630eed..ebf033a462a3 100644 --- a/packages/server-utils/src/ai/anthropic-ai/tool-runner.ts +++ b/packages/server-utils/src/ai/anthropic-ai/tool-runner.ts @@ -2,23 +2,35 @@ import type { Span, WrappedFunction } from '@sentry/core'; import { captureException, fill, + getActiveSpan, + getCurrentScope, + getDefaultCurrentScope, + getDefaultIsolationScope, + getIsolationScope, getOriginalFunction, handleCallbackErrors, SPAN_STATUS_ERROR, + startInactiveSpan, + startNewTrace, startSpan, stringify, + withActiveSpan, + withScope, } from '@sentry/core'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, GEN_AI_TOOL_CALL_ARGUMENTS, GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_DESCRIPTION, GEN_AI_TOOL_NAME, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; +import { GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../core/gen-ai-attributes'; import { resolveAIRecordingOptions } from '../core/utils'; +import { withCleanScopes } from '../../utils/withCleanScopes'; import { ANTHROPIC_AI_ORIGIN } from './constants'; import type { AnthropicAiOptions } from './types'; @@ -26,7 +38,7 @@ type AnyFunction = (...args: unknown[]) => unknown; type RunnableTool = { name: string; description?: unknown }; type RunContext = { toolUse?: { id?: unknown }; toolUseBlock?: { id?: unknown }; signal?: AbortSignal } | undefined; type ToolRunner = { params?: { tools?: unknown } }; -type SessionToolRunner = { tools?: unknown }; +type SessionToolRunner = { tools?: unknown; sessionId?: unknown }; /** Wrap the `toolRunner()` method of `owner` so the runners it returns trace their tool calls. */ export function instrumentToolRunnerFactory( @@ -61,14 +73,81 @@ export function instrumentToolRunner(runner: unknown, options: AnthropicAiOption }); } -/** Give each tool call of a Managed Agents session runner a `gen_ai.execute_tool` span. */ +/** Give each tool call of a Managed Agents session runner an `execute_tool` span, under one `invoke_agent` span per session. */ export function instrumentSessionToolRunner(runner: unknown, options: AnthropicAiOptions): void { if (!runner || typeof runner !== 'object') { return; } + const { tools, sessionId } = runner as SessionToolRunner; // The session runner has no `addTools()`: it only runs the tools it was created with. - instrumentTools((runner as SessionToolRunner).tools, options); + instrumentTools(tools, options); + wrapInPlace(runner, Symbol.asyncIterator, (iterate, thisArg) => + traceSessionLoop( + iterate.call(thisArg) as AsyncIterator, + typeof sessionId === 'string' ? sessionId : undefined, + ), + ); +} + +/** Wraps the SDK's session iterator so the whole session runs inside one `invoke_agent` span. */ +async function* traceSessionLoop( + sdkIterator: AsyncIterator, + sessionId: string | undefined, +): AsyncGenerator { + const { span, firstResult } = startSession(sdkIterator, sessionId); + + try { + let result = await firstResult; + while (!result.done) { + yield result.value; + result = await sdkIterator.next(); + } + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + throw error; + } finally { + try { + // After a `break`, closing the SDK loop waits for its in-flight tools, so the span ends after them. + await sdkIterator.return?.(); + } finally { + span.end(); + } + } +} + +// The SDK starts its tool loop on the first `next()`, so that call runs in the session and every tool inherits it. +function startSession( + sdkIterator: AsyncIterator, + sessionId: string | undefined, +): { span: Span; firstResult: Promise> } { + const start = (): { span: Span; firstResult: Promise> } => + withScope(scope => { + // A tool shared between runners can't tell its session, so the id reaches its spans through the scope, unless + // the app set one with `Sentry.setConversationId()`. + if (sessionId && !scope.getScopeData().conversationId && !getIsolationScope().getScopeData().conversationId) { + scope.setConversationId(sessionId); + } + + const span = startInactiveSpan({ + name: 'invoke_agent', + op: GEN_AI_INVOKE_AGENT, + attributes: { + [SENTRY_ORIGIN]: ANTHROPIC_AI_ORIGIN, + [GEN_AI_OPERATION_NAME]: 'invoke_agent', + [GEN_AI_PROVIDER_NAME]: 'anthropic', + ...(sessionId ? { [GEN_AI_CONVERSATION_ID]: sessionId } : {}), + }, + }); + // `async`, so a `next()` that throws rejects `firstResult` and still ends the span. + return { span, firstResult: withActiveSpan(span, async () => sdkIterator.next()) }; + }); + + // At top level all sessions would share the process's trace, so each gets its own; anywhere else it stays in the + // caller's. + return !getActiveSpan() && getCurrentScope() === getDefaultCurrentScope() + ? withCleanScopes(getDefaultIsolationScope().clone(), () => startNewTrace(start)) + : start(); } function instrumentTools(tools: unknown, options: AnthropicAiOptions): void { @@ -86,10 +165,10 @@ function instrumentTools(tools: unknown, options: AnthropicAiOptions): void { function wrapInPlace( owner: object, - key: string, + key: string | symbol, call: (original: AnyFunction, thisArg: unknown, args: unknown[]) => unknown, ): void { - const current = (owner as Record)[key]; + const current = (owner as Record)[key]; if (typeof current === 'function' && !getOriginalFunction(current as WrappedFunction)) { fill( owner, diff --git a/packages/server-utils/src/ai/anthropic-ai/utils.ts b/packages/server-utils/src/ai/anthropic-ai/utils.ts index 4a913749449c..65001dba746c 100644 --- a/packages/server-utils/src/ai/anthropic-ai/utils.ts +++ b/packages/server-utils/src/ai/anthropic-ai/utils.ts @@ -1,4 +1,4 @@ -import { captureException, SPAN_STATUS_ERROR, stringify } from '@sentry/core'; +import { captureException, isObjectLike, SPAN_STATUS_ERROR, stringify } from '@sentry/core'; import type { Span, SpanStatusType } from '@sentry/core'; import { GEN_AI_INPUT_MESSAGES, GEN_AI_SYSTEM_INSTRUCTIONS } from '@sentry/conventions/attributes'; import { extractSystemInstructions } from '../core/utils'; @@ -64,13 +64,25 @@ export function handleResponseError(span: Span, response: AnthropicAiResponse): } } +/** Anthropic accepts the system prompt as a string or as an array of text blocks. */ +function systemToText(system: unknown): string | undefined { + if (!Array.isArray(system)) { + return typeof system === 'string' ? system : undefined; + } + return system + .map(block => (isObjectLike(block) ? block.text : undefined)) + .filter(text => typeof text === 'string' && text) + .join('\n'); +} + /** * Include the system prompt in the messages list, if available */ export function messagesFromParams(params: Record): unknown[] { const { system, messages, input, prompt } = params; - const systemMessages = typeof system === 'string' ? [{ role: 'system', content: params.system }] : []; + const systemText = systemToText(system); + const systemMessages = systemText ? [{ role: 'system', content: systemText }] : []; const inputParamMessages = Array.isArray(input) ? input : input != null ? [input] : undefined; diff --git a/packages/server-utils/src/ai/flue/index.ts b/packages/server-utils/src/ai/flue/index.ts index b5f969c8a16e..94db427df5ca 100644 --- a/packages/server-utils/src/ai/flue/index.ts +++ b/packages/server-utils/src/ai/flue/index.ts @@ -4,7 +4,6 @@ import { getActiveSpan, getDefaultIsolationScope, getIsolationScope, - GLOBAL_OBJ, LRUMap, startNewTrace, startSpan, @@ -19,6 +18,7 @@ import { import type { GenAiOptions } from '../core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; import { skipPiAiProviderIntegrations } from '../pi-ai/providers'; +import { isCloudflareWorkers } from '../../utils/runtime'; import { withCleanScopes } from '../../utils/withCleanScopes'; import { FLUE_INSTRUMENTATION_KEY, FLUE_OPERATION, FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants'; import type { SpanTracker } from './utils'; @@ -46,6 +46,8 @@ interface Admission { * (https://github.com/withastro/flue/issues/813). */ function runInAdmissionContext(admission: Admission | undefined, next: () => T): T { + // Cloudflare runs each turn from a Durable Object alarm, which already starts a trace of its own, so + // there is no leaked context to undo there. if (isCloudflareWorkers()) { return next(); } @@ -59,14 +61,6 @@ function runInAdmissionContext(admission: Admission | undefined, next: () => ); } -/** - * Cloudflare runs each turn from a Durable Object alarm, which already starts a trace of its own, so - * there is no leaked context to undo there. - */ -function isCloudflareWorkers(): boolean { - return GLOBAL_OBJ.navigator?.userAgent === 'Cloudflare-Workers'; -} - /** * Build the object to hand to `instrument()` from `@flue/runtime`. * diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index e8c002a921f8..8b0477ae40d4 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -333,7 +333,12 @@ function instrumentMethod( addPrivateRequestAttributes(span, attributeParams, operationName); } const stream = await target.apply(context, args); - return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; + return instrumentStream( + stream, + span, + Boolean(options.recordOutputs), + (attributeParams?.config as { abortSignal?: AbortSignal } | undefined)?.abortSignal, + ) as R; } catch (error) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); span.end(); diff --git a/packages/server-utils/src/ai/google-genai/streaming.ts b/packages/server-utils/src/ai/google-genai/streaming.ts index 3a2f720f2628..3b19a991e0d0 100644 --- a/packages/server-utils/src/ai/google-genai/streaming.ts +++ b/packages/server-utils/src/ai/google-genai/streaming.ts @@ -122,6 +122,7 @@ export async function* instrumentStream( stream: AsyncIterable, span: Span, recordOutputs: boolean, + abortSignal?: AbortSignal, ): AsyncGenerator { const state: StreamingState = { responseTexts: [], @@ -135,6 +136,9 @@ export async function* instrumentStream( processChunk(chunk, state, recordOutputs, span); yield chunk; } + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: abortSignal?.aborted ? 'cancelled' : 'internal_error' }); + throw error; } finally { if (recordOutputs) { setOutputMessagesAttribute(span, state.outputParts); diff --git a/packages/server-utils/src/ai/langchain/utils.ts b/packages/server-utils/src/ai/langchain/utils.ts index 8ae219e42437..405340e71c56 100644 --- a/packages/server-utils/src/ai/langchain/utils.ts +++ b/packages/server-utils/src/ai/langchain/utils.ts @@ -505,7 +505,7 @@ export function extractLlmResponseAttributes( export function getAgentNameFromMetadata(metadata?: Record): Record { const attrs: Record = {}; - // lc_agent_name is injected by instrumentCompiledGraphInvoke (langgraph integration) + // lc_agent_name is injected by the langgraph invoke()/stream() wrappers (`prepareAgentSpan`) const agentName = metadata?.lc_agent_name; if (typeof agentName === 'string') { attrs[GEN_AI_AGENT_NAME] = agentName; @@ -606,24 +606,29 @@ function containsSentryHandler(handlers: unknown[]): boolean { * @internal Exported for cross-package instrumentation. */ export function _INTERNAL_mergeLangChainCallbackHandler(existing: unknown, sentryHandler: unknown): unknown { + const handlers = isCallbackManager(existing) ? (existing.handlers ?? []) : toHandlerArray(existing); + return containsSentryHandler(handlers) ? existing : _INTERNAL_addLangChainCallbackHandler(existing, sentryHandler); +} + +/** + * Add `handler` to a given set of LangChain callbacks or callback manager, without deduplicating it. + * A callback manager is copied rather than mutated. + * @internal Exported for cross-package instrumentation. + */ +export function _INTERNAL_addLangChainCallbackHandler(existing: unknown, handler: unknown): unknown { if (!existing) { - return [sentryHandler]; + return [handler]; } if (isCallbackManager(existing)) { - if (containsSentryHandler(existing.handlers ?? [])) { - return existing; - } - const copied = existing.copy() as { addHandler: (handler: unknown, inherit?: boolean) => void }; - copied.addHandler(sentryHandler, true); + copied.addHandler(handler, true); return copied; } - const handlers = Array.isArray(existing) ? existing : [existing]; - if (containsSentryHandler(handlers)) { - return existing; - } + return [...toHandlerArray(existing), handler]; +} - return [...handlers, sentryHandler]; +function toHandlerArray(existing: unknown): unknown[] { + return Array.isArray(existing) ? existing : existing ? [existing] : []; } diff --git a/packages/server-utils/src/ai/langgraph/agent-span.ts b/packages/server-utils/src/ai/langgraph/agent-span.ts new file mode 100644 index 000000000000..acae26fda9cf --- /dev/null +++ b/packages/server-utils/src/ai/langgraph/agent-span.ts @@ -0,0 +1,144 @@ +import { stringify } from '@sentry/core'; +import type { Span, SpanAttributes } from '@sentry/core'; +import { + GEN_AI_AGENT_NAME, + GEN_AI_INPUT_MESSAGES, + GEN_AI_OPERATION_NAME, + GEN_AI_PIPELINE_NAME, + GEN_AI_REQUEST_MODEL, + GEN_AI_SYSTEM_INSTRUCTIONS, + GEN_AI_TOOL_DEFINITIONS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; +import { extractSystemInstructions } from '../core/utils'; +import type { BaseChatModel, LangChainMessage } from '../langchain/types'; +import { + _INTERNAL_mergeLangChainCallbackHandler, + getConversationIdFromMetadata, + getConversationIdMetadataFromConfig, + getInheritedLangChainCallbacks, + normalizeLangChainMessages, +} from '../langchain/utils'; +import { LANGGRAPH_ORIGIN } from './constants'; +import type { CompiledGraph, LangGraphOptions } from './types'; +import { extractToolsFromCompiledGraph } from './utils'; + +// LangGraph's invoke() calls this.stream() before its first await, so a graph set only for that synchronous +// stretch marks exactly invoke's own stream() call. A stream() started later in the run gets its own span. +let invokingGraph: object | undefined; + +/** Runs the original invoke() of `graph`, so its internal stream() call can be recognised. */ +export function callInvoke(graph: object, invoke: () => T): T { + const previous = invokingGraph; + invokingGraph = graph; + try { + return invoke(); + } finally { + invokingGraph = previous; + } +} + +/** Whether this stream() call on `graph` is the one its invoke() makes internally. */ +export function isInvokeInternalStream(graph: object): boolean { + return invokingGraph === graph; +} + +/** Name and initial attributes of an agent span, from the call's config. */ +export function getAgentSpanOptions( + args: unknown[], + extraAttributes?: SpanAttributes, +): { name: string; attributes: SpanAttributes } { + const config = args.length > 1 ? (args[1] as Record | undefined) : undefined; + return { + name: 'invoke_agent', + attributes: { + // Set before `spanStart`, so an id from `Sentry.setConversationId()` wins, as on the child spans + ...getConversationIdFromMetadata(config?.configurable as Record | undefined), + [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, + [SENTRY_OP]: GEN_AI_INVOKE_AGENT, + [GEN_AI_OPERATION_NAME]: 'invoke_agent', + ...extraAttributes, + }, + }; +} + +/** Sets the request attributes on an agent span and injects the callback handler into the call's config. */ +export function prepareAgentSpan( + span: Span, + args: unknown[], + graphInstance: CompiledGraph, + compileOptions: Record, + options: LangGraphOptions, + llm: BaseChatModel | null | undefined, + sentryCallbackHandler: unknown, +): LangChainMessage[] | null { + const graphName = compileOptions?.name; + + if (graphName && typeof graphName === 'string') { + span.setAttribute(GEN_AI_PIPELINE_NAME, graphName); + span.setAttribute(GEN_AI_AGENT_NAME, graphName); + span.updateName(`invoke_agent ${graphName}`); + } + + const modelName = llm?.modelName ?? llm?.model; + if (modelName) { + span.setAttribute(GEN_AI_REQUEST_MODEL, modelName); + } + + // A copy, so nothing is written back to a config object the caller reuses for other calls. + const config: Record = { ...(args[1] as Record | undefined) }; + args[1] = config; + // Explicit callbacks replace the parent run's, which LangChain would otherwise inherit. + const callbacks = config.callbacks ?? getInheritedLangChainCallbacks(); + if (callbacks) { + config.callbacks = callbacks; + } + + if (sentryCallbackHandler) { + const existingMetadata = (config.metadata ?? {}) as Record; + config.metadata = { + ...getConversationIdMetadataFromConfig(config), + ...existingMetadata, + __sentry_langgraph__: true, + ...(typeof graphName === 'string' ? { lc_agent_name: graphName } : {}), + }; + + config.callbacks = _INTERNAL_mergeLangChainCallbackHandler(config.callbacks, sentryCallbackHandler); + } + + const recordInputs = options.recordInputs; + + const tools = recordInputs ? extractToolsFromCompiledGraph(graphInstance) : null; + if (tools) { + span.setAttribute(GEN_AI_TOOL_DEFINITIONS, JSON.stringify(tools)); + } + + // Custom state annotations have no `messages` array, the whole state is recorded instead. + const inputState = args[0] as { messages?: LangChainMessage[]; lg_name?: string } | null | undefined; + const inputMessages = Array.isArray(inputState?.messages) ? inputState.messages : null; + // `new Command({ resume })` resumes an interrupted run and carries no user turn (LangGraph + // tags it `lg_name: 'Command'`), so skip it like a `null` resume rather than recording the + // control object as a message nobody wrote. + const isResumeCommand = inputState?.lg_name === 'Command'; + + if (recordInputs) { + if (inputMessages) { + const normalizedMessages = normalizeLangChainMessages(inputMessages); + const { systemInstructions, filteredMessages } = extractSystemInstructions(normalizedMessages); + + if (systemInstructions) { + span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, systemInstructions); + } + + span.setAttributes({ + [GEN_AI_INPUT_MESSAGES]: stringify(filteredMessages), + }); + } else if (inputState && typeof inputState === 'object' && !isResumeCommand) { + span.setAttribute(GEN_AI_INPUT_MESSAGES, stringify([{ role: 'user', content: stringify(inputState) }])); + } + } + + return inputMessages; +} diff --git a/packages/server-utils/src/ai/langgraph/index.ts b/packages/server-utils/src/ai/langgraph/index.ts index 0b1dc9a566d6..82af1fa78ab6 100644 --- a/packages/server-utils/src/ai/langgraph/index.ts +++ b/packages/server-utils/src/ai/langgraph/index.ts @@ -1,42 +1,36 @@ -/* eslint-disable typescript-eslint/no-deprecated */ -import { SPAN_STATUS_ERROR, startSpan, stringify } from '@sentry/core'; -import { - GEN_AI_AGENT_NAME, - GEN_AI_INPUT_MESSAGES, - GEN_AI_OPERATION_NAME, - GEN_AI_PIPELINE_NAME, - GEN_AI_REQUEST_MODEL, - GEN_AI_SYSTEM_INSTRUCTIONS, - GEN_AI_TOOL_DEFINITIONS, - SENTRY_OP, - SENTRY_ORIGIN, -} from '@sentry/conventions/attributes'; -import { GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; -import { extractSystemInstructions, resolveAIRecordingOptions } from '../core/utils'; +import { SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; +import { resolveAIRecordingOptions } from '../core/utils'; import { createLangChainCallbackHandler } from '../langchain'; -import type { BaseChatModel, LangChainMessage } from '../langchain/types'; -import { normalizeLangChainMessages } from '../langchain/utils'; -import { LANGGRAPH_ORIGIN } from './constants'; +import type { BaseChatModel } from '../langchain/types'; +import { callInvoke, getAgentSpanOptions, prepareAgentSpan } from './agent-span'; +import { instrumentCompiledGraphStream } from './stream'; import type { CompiledGraph, LangGraphOptions } from './types'; -import { - extractAgentNameFromParams, - extractLLMFromParams, - extractToolsFromCompiledGraph, - setResponseAttributes, - wrapToolsWithSpans, -} from './utils'; -import { - getConversationIdMetadataFromConfig, - _INTERNAL_mergeLangChainCallbackHandler, - getConversationIdFromMetadata, -} from '../langchain/utils'; +import { extractAgentNameFromParams, extractLLMFromParams, setResponseAttributes, wrapToolsWithSpans } from './utils'; -let _insideCreateReactAgent = false; +// Set while a manual wrapper runs the original `compile`/`createReactAgent`. The integration's channel hooks +// fire synchronously inside that call, and skip the graph so the options passed explicitly win. +let _insideManualInstrumentation = false; const SENTRY_PATCHED = '__sentry_patched__'; +const INSTRUMENTED_GRAPHS = new WeakSet(); + +/** Whether manual `instrumentStateGraph()`/`instrumentCreateReactAgent()` is creating a graph right now. */ +export function isInsideManualLangGraphInstrumentation(): boolean { + return _insideManualInstrumentation; +} + +function runManually(callback: () => T): T { + const previous = _insideManualInstrumentation; + _insideManualInstrumentation = true; + try { + return callback(); + } finally { + _insideManualInstrumentation = previous; + } +} /** - * Instruments StateGraph's compile method to wrap the returned compiled graph's invoke() with a + * Instruments StateGraph's compile method to wrap the returned compiled graph's invoke() and stream() with a * `gen_ai.invoke_agent` span. */ export function instrumentStateGraphCompile( @@ -54,26 +48,14 @@ export function instrumentStateGraphCompile( const wrapped = new Proxy(originalCompile, { apply(target, thisArg, args: unknown[]): CompiledGraph { - // Skip when called from within createReactAgent to avoid duplicate instrumentation - if (_insideCreateReactAgent) { + // The createReactAgent wrapper instruments the graph it compiles internally, with the agent's name and model. + if (_insideManualInstrumentation) { return Reflect.apply(target, thisArg, args); } - const compiledGraph = Reflect.apply(target, thisArg, args); + const compiledGraph = runManually(() => Reflect.apply(target, thisArg, args)); const compileOptions = args.length > 0 ? (args[0] as Record) : {}; - - // Instrument agent invoke method on the compiled graph - const originalInvoke = compiledGraph.invoke; - if (originalInvoke && typeof originalInvoke === 'function') { - compiledGraph.invoke = instrumentCompiledGraphInvoke( - originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise, - compiledGraph, - compileOptions, - options, - undefined, - sentryHandler, - ); - } + instrumentCompiledGraph(compiledGraph, compileOptions, options, undefined, sentryHandler); return compiledGraph; }, @@ -83,6 +65,45 @@ export function instrumentStateGraphCompile( return wrapped; } +/** + * Wraps a compiled graph's `invoke()` and `stream()` with `gen_ai.invoke_agent` spans, once per graph. + */ +export function instrumentCompiledGraph( + graph: unknown, + compileOptions: Record, + options: LangGraphOptions, + llm: BaseChatModel | null | undefined, + sentryCallbackHandler: unknown, +): void { + if (!graph || typeof graph !== 'object' || INSTRUMENTED_GRAPHS.has(graph)) { + return; + } + INSTRUMENTED_GRAPHS.add(graph); + + const compiledGraph = graph as CompiledGraph; + const { invoke, stream } = compiledGraph; + if (typeof invoke === 'function') { + compiledGraph.invoke = instrumentCompiledGraphInvoke( + invoke.bind(compiledGraph), + compiledGraph, + compileOptions, + options, + llm, + sentryCallbackHandler, + ); + } + if (typeof stream === 'function') { + compiledGraph.stream = instrumentCompiledGraphStream( + stream.bind(compiledGraph), + compiledGraph, + compileOptions, + options, + llm, + sentryCallbackHandler, + ); + } +} + /** * Instruments CompiledGraph's invoke method to create spans for agent invocation * @@ -98,102 +119,31 @@ export function instrumentCompiledGraphInvoke( ): (...args: unknown[]) => Promise { return new Proxy(originalInvoke, { apply(target, thisArg, args: unknown[]): Promise { - const modelName = llm?.modelName ?? llm?.model; - const config = args.length > 1 ? (args[1] as Record | undefined) : undefined; - return startSpan( - { - name: 'invoke_agent', - attributes: { - // Set before `spanStart`, so an id from `Sentry.setConversationId()` wins, as on the child spans - ...getConversationIdFromMetadata(config?.configurable as Record | undefined), - [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, - [SENTRY_OP]: GEN_AI_INVOKE_AGENT, - [GEN_AI_OPERATION_NAME]: 'invoke_agent', - }, - }, - async span => { - try { - const graphName = compileOptions?.name; - - if (graphName && typeof graphName === 'string') { - span.setAttribute(GEN_AI_PIPELINE_NAME, graphName); - span.setAttribute(GEN_AI_AGENT_NAME, graphName); - span.updateName(`invoke_agent ${graphName}`); - } - - if (modelName) { - span.setAttribute(GEN_AI_REQUEST_MODEL, modelName); - } - - // Inject callback handler and agent name into invoke config - if (sentryCallbackHandler) { - const invokeConfig = (args[1] ?? {}) as Record; - args[1] = invokeConfig; - - const existingMetadata = (invokeConfig.metadata ?? {}) as Record; - invokeConfig.metadata = { - ...getConversationIdMetadataFromConfig(invokeConfig), - ...existingMetadata, - __sentry_langgraph__: true, - ...(typeof graphName === 'string' ? { lc_agent_name: graphName } : {}), - }; - - invokeConfig.callbacks = _INTERNAL_mergeLangChainCallbackHandler( - invokeConfig.callbacks, - sentryCallbackHandler, - ); - } - - const recordInputs = options.recordInputs; - const recordOutputs = options.recordOutputs; - - // Extract available tools from the graph instance - const tools = recordInputs ? extractToolsFromCompiledGraph(graphInstance) : null; - if (tools) { - span.setAttribute(GEN_AI_TOOL_DEFINITIONS, JSON.stringify(tools)); - } - - // Custom state annotations have no `messages` array, the whole state is recorded instead. - const inputState = args[0] as { messages?: LangChainMessage[]; lg_name?: string } | null | undefined; - const inputMessages = Array.isArray(inputState?.messages) ? inputState.messages : null; - // `new Command({ resume })` resumes an interrupted run and carries no user turn (LangGraph - // tags it `lg_name: 'Command'`), so skip it like a `null` resume rather than recording the - // control object as a message nobody wrote. - const isResumeCommand = inputState?.lg_name === 'Command'; - - if (recordInputs) { - if (inputMessages) { - const normalizedMessages = normalizeLangChainMessages(inputMessages); - const { systemInstructions, filteredMessages } = extractSystemInstructions(normalizedMessages); - - if (systemInstructions) { - span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, systemInstructions); - } - - span.setAttributes({ - [GEN_AI_INPUT_MESSAGES]: stringify(filteredMessages), - }); - } else if (inputState && typeof inputState === 'object' && !isResumeCommand) { - span.setAttribute(GEN_AI_INPUT_MESSAGES, stringify([{ role: 'user', content: stringify(inputState) }])); - } - } - - // Call original invoke - const result = await Reflect.apply(target, thisArg, args); - - if (recordOutputs) { - setResponseAttributes(span, inputMessages, result); - } - - return result; - } catch (error) { - // The error is rethrown to the caller (invoke() rejects), so we only mark the span failed - // and do not record it. - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - throw error; + return startSpan(getAgentSpanOptions(args), async span => { + try { + const inputMessages = prepareAgentSpan( + span, + args, + graphInstance, + compileOptions, + options, + llm, + sentryCallbackHandler, + ); + const result = await callInvoke(graphInstance, () => Reflect.apply(target, thisArg, args)); + + if (options.recordOutputs) { + setResponseAttributes(span, inputMessages, result); } - }, - ); + + return result; + } catch (error) { + // The error is rethrown to the caller (invoke() rejects), so we only mark the span failed + // and do not record it. + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + throw error; + } + }); }, }); } @@ -223,32 +173,10 @@ export function instrumentCreateReactAgent( wrapToolsWithSpans(params.tools, resolvedOptions, agentName ?? undefined); } - // Suppress StateGraph.compile instrumentation inside createReactAgent - _insideCreateReactAgent = true; - let compiledGraph: CompiledGraph; - try { - compiledGraph = Reflect.apply(target, thisArg, args); - } finally { - _insideCreateReactAgent = false; - } - - // Wrap invoke() on the returned compiled graph - const originalInvoke = compiledGraph.invoke; - if (originalInvoke && typeof originalInvoke === 'function') { - const compileOptions: Record = {}; - if (agentName) { - compileOptions.name = agentName; - } + const compiledGraph = runManually(() => Reflect.apply(target, thisArg, args)); - compiledGraph.invoke = instrumentCompiledGraphInvoke( - originalInvoke.bind(compiledGraph) as (...args: unknown[]) => Promise, - compiledGraph, - compileOptions, - resolvedOptions, - llm, - sentryHandler, - ); - } + const compileOptions: Record = agentName ? { name: agentName } : {}; + instrumentCompiledGraph(compiledGraph, compileOptions, resolvedOptions, llm, sentryHandler); return compiledGraph; }, diff --git a/packages/server-utils/src/ai/langgraph/stream.ts b/packages/server-utils/src/ai/langgraph/stream.ts new file mode 100644 index 000000000000..3370eb9fe109 --- /dev/null +++ b/packages/server-utils/src/ai/langgraph/stream.ts @@ -0,0 +1,140 @@ +import { SPAN_STATUS_ERROR, startSpanManual } from '@sentry/core'; +import { GEN_AI_RESPONSE_STREAMING } from '@sentry/conventions/attributes'; +import type { BaseChatModel, LangChainMessage } from '../langchain/types'; +import { _INTERNAL_addLangChainCallbackHandler } from '../langchain/utils'; +import { getAgentSpanOptions, isInvokeInternalStream, prepareAgentSpan } from './agent-span'; +import type { CompiledGraph, LangGraphOptions } from './types'; +import { setResponseAttributes } from './utils'; + +/** + * Instruments CompiledGraph's stream method with a `gen_ai.invoke_agent` span that lasts for the graph run. + * + * LangGraph runs the graph as soon as `stream()` is called, whether or not the stream is read, so the span + * ends on the run's own end or error callback rather than when the stream is consumed. + */ +export function instrumentCompiledGraphStream( + originalStream: (...args: unknown[]) => Promise, + graphInstance: CompiledGraph, + compileOptions: Record, + options: LangGraphOptions, + llm?: BaseChatModel | null, + sentryCallbackHandler?: unknown, +): (...args: unknown[]) => Promise { + return new Proxy(originalStream, { + apply(target, thisArg, args: unknown[]): Promise { + if (isInvokeInternalStream(graphInstance)) { + return Reflect.apply(target, thisArg, args); + } + + return startSpanManual(getAgentSpanOptions(args, { [GEN_AI_RESPONSE_STREAMING]: true }), async span => { + let inputMessages: LangChainMessage[] | null = null; + let stream: { signal?: AbortSignal } | undefined; + let ended = false; + const finish = (outcome?: { outputs: unknown } | { error: unknown }): void => { + if (ended) { + return; + } + ended = true; + try { + if (outcome && 'error' in outcome) { + // A cancel or abort by the caller is not a failed run. LangGraph reports both, and step + // timeouts too, as `Error('Abort')`, so only the signals tell them apart. + if (!stream?.signal?.aborted && !(args[1] as { signal?: AbortSignal } | undefined)?.signal?.aborted) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + } + } else if (outcome && options.recordOutputs) { + setResponseAttributes(span, inputMessages, outcome.outputs); + } + } catch { + // Recording must never change the outcome of the run. + } + span.end(); + inputMessages = null; + }; + + // The root run starts before any of its children, so it is the first run this per-call listener sees. + let rootRunId: string | undefined; + const listener = { + name: 'SentryLangGraphStreamListener', + awaitHandlers: true, + handleChainStart(_chain: unknown, _inputs: unknown, runId: string): void { + rootRunId ??= runId; + }, + handleChainEnd(outputs: unknown, runId: string): void { + if (runId === rootRunId) { + finish({ outputs }); + } + }, + handleChainError(error: unknown, runId: string): void { + if (runId === rootRunId) { + finish({ error }); + } + }, + copy() { + return listener; + }, + }; + + try { + inputMessages = prepareAgentSpan( + span, + args, + graphInstance, + compileOptions, + options, + llm, + sentryCallbackHandler, + ); + // `prepareAgentSpan` gave the call its own copy of the config, so the listener stays on this call. + const config = args[1] as Record; + config.callbacks = _INTERNAL_addLangChainCallbackHandler(config.callbacks, listener); + stream = (await Reflect.apply(target, thisArg, args)) as typeof stream; + } catch (error) { + finish({ error }); + throw error; + } + + if (rootRunId === undefined) { + // Not a LangGraph run we can follow, so don't leave the span open. + finish(); + } else { + endOnEarlyExit(stream, finish); + } + return stream; + }); + }, + }); +} + +// After a `break`, LangGraph keeps running the graph but never reports the run's end, so the span ends when +// the caller stops. Nodes still running at that point start spans after the agent span ended. +function endOnEarlyExit(stream: unknown, finish: () => void): void { + if (!stream || typeof stream !== 'object') { + return; + } + const target = stream as { return?: (...args: unknown[]) => unknown; cancel?: (...args: unknown[]) => unknown }; + const { return: originalReturn, cancel: originalCancel } = target; + try { + if (typeof originalReturn === 'function') { + target.return = function (this: unknown, ...args: unknown[]): unknown { + try { + return originalReturn.apply(this, args); + } finally { + finish(); + } + }; + } + if (typeof originalCancel === 'function') { + target.cancel = function (this: unknown, ...args: unknown[]): unknown { + const result = originalCancel.apply(this, args); + Promise.resolve(result).then( + () => finish(), + () => undefined, + ); + return result; + }; + } + } catch { + finish(); + } +} diff --git a/packages/server-utils/src/ai/langgraph/types.ts b/packages/server-utils/src/ai/langgraph/types.ts index 153fa9b4caba..f8ac291c5ee4 100644 --- a/packages/server-utils/src/ai/langgraph/types.ts +++ b/packages/server-utils/src/ai/langgraph/types.ts @@ -61,6 +61,7 @@ export interface StateGraphBuilder { export interface CompiledGraph { [key: string]: unknown; invoke?: (...args: unknown[]) => Promise; + stream?: (...args: unknown[]) => Promise; name?: string; graph_name?: string; lc_kwargs?: { diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index 1b261472554b..adb3dfb1850a 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -219,7 +219,7 @@ export function extractModelMetadata(span: Span, message: LangChainMessage): voi } if (metadata.finish_reason && typeof metadata.finish_reason === 'string') { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [metadata.finish_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([metadata.finish_reason])); } } } diff --git a/packages/server-utils/src/ai/mastra/utils.ts b/packages/server-utils/src/ai/mastra/utils.ts index 5fa9f1b62316..3f6d32ed8424 100644 --- a/packages/server-utils/src/ai/mastra/utils.ts +++ b/packages/server-utils/src/ai/mastra/utils.ts @@ -57,7 +57,7 @@ export function getOperation(spanType: MastraSpanType): { op: string; operationN /** * `{operation} {identifier}` per the gen_ai name templates. Mastra's own span name is never used. - * https://getsentry.github.io/sentry-conventions/names/ + * https://conventions.sentry.dev/names/ */ export function getSpanName(span: MastraExportedSpan): string { const operationName = getOperation(span.type)?.operationName ?? span.type; diff --git a/packages/server-utils/src/ai/openai/streaming.ts b/packages/server-utils/src/ai/openai/streaming.ts index 15686abfa072..467f9d0b559f 100644 --- a/packages/server-utils/src/ai/openai/streaming.ts +++ b/packages/server-utils/src/ai/openai/streaming.ts @@ -282,6 +282,9 @@ export async function* instrumentStream( } yield event; } + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + throw error; } finally { const allToolCalls = [...state.chatCompletionToolCalls.values(), ...state.responsesApiToolCalls]; endStreamSpan(span, { ...state, toolCalls: allToolCalls }, recordOutputs); diff --git a/packages/server-utils/src/exports.ts b/packages/server-utils/src/exports.ts index 7b36665f97cd..d9fae4eb0f29 100644 --- a/packages/server-utils/src/exports.ts +++ b/packages/server-utils/src/exports.ts @@ -5,6 +5,7 @@ export { openTelemetryIntegration, getOtlpTracesEndpoint } from './opentelemetry export * from './ai'; export { getSqlQuerySummary, sanitizeSqlQuery, getSqlQueryAttributes, getSqlSpanName } from './utils/sql'; export type { SqlDialect } from './utils/sql'; +export { isBun, isCloudflareWorkers, isDeno } from './utils/runtime'; export { instrumentPostgresJsSql } from './integrations/postgresjs-wrapper'; export type { PostgresConnectionContext } from './integrations/postgresjs-wrapper'; diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 74e4dd25e64a..77c1fd37c2ea 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -61,6 +61,11 @@ export { piDurableIntegration } from './integrations/pi-durable'; export type { FlueOptions } from './ai/flue'; export { mastraIntegration } from './integrations/mastra'; export { mcpServerIntegration } from './integrations/mcp-server'; +export { + instrumentTunnelServers as _INTERNAL_instrumentTunnelServers, + makeServeTunnelIntegration, +} from './integrations/serveTunnel'; +export { instrumentTunnelServer as _INTERNAL_instrumentTunnelServer } from './utils/tunnelServer'; export { SentryMastraExporter } from './ai/mastra'; export { lruMemoizerIntegration } from './integrations/lru-memoizer'; export { mongoIntegration } from './integrations/mongodb'; diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index 10951179dd04..e98c00a6ee6f 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -108,6 +108,7 @@ function instrumentAwsSdk(servicesExtensions: ServicesExtensions): void { command.input = {}; } const normalizedRequest = normalizeV3Request(serviceName, commandName, command.input, undefined); + normalizedRequest.abortSignal = (data.arguments[1] as { abortSignal?: AbortSignal } | undefined)?.abortSignal; const requestMetadata = servicesExtensions.requestPreSpanHook(normalizedRequest); // `rpc` matches what the exporter infers from `rpc.service` for the OTel aws-sdk spans; diff --git a/packages/server-utils/src/integrations/aws-sdk/services/bedrock-runtime.ts b/packages/server-utils/src/integrations/aws-sdk/services/bedrock-runtime.ts index 98a97d408765..c7ba139f9f48 100644 --- a/packages/server-utils/src/integrations/aws-sdk/services/bedrock-runtime.ts +++ b/packages/server-utils/src/integrations/aws-sdk/services/bedrock-runtime.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { debug } from '@sentry/core'; +import { debug, SPAN_STATUS_ERROR } from '@sentry/core'; import { GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, @@ -9,10 +9,13 @@ import { GEN_AI_REQUEST_TEMPERATURE, GEN_AI_REQUEST_TOP_P, GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, } from '@sentry/conventions/attributes'; import { GEN_AI_CHAT, GEN_AI_GENERATE_CONTENT } from '@sentry/conventions/op'; +import { getTokenUsageAttributes } from '../../../ai/core/utils'; import { DEBUG_BUILD } from '../../../debug-build'; import { GEN_AI_OPERATION_NAME_VALUE_CHAT, @@ -28,6 +31,8 @@ interface TokenUsage { inputTokens: number | undefined; outputTokens: number | undefined; totalTokens: number | undefined; + cacheReadInputTokens?: number; + cacheWriteInputTokens?: number; } interface ConverseStreamOutput { @@ -137,8 +142,8 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { spanName += ` ${modelId}`; } - if (request.commandInput?.body) { - const requestBody = JSON.parse(request.commandInput.body); + const requestBody = parseRequestBody(request.commandInput?.body); + if (requestBody && typeof modelId === 'string') { if (modelId.includes('amazon.titan')) { if (requestBody.textGenerationConfig?.temperature !== undefined) { spanAttributes[GEN_AI_REQUEST_TEMPERATURE] = requestBody.textGenerationConfig.temperature; @@ -254,13 +259,28 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { const { stopReason, usage } = response.data; setStopReason(span, stopReason); - setUsage(span, usage); + span.setAttributes( + getTokenUsageAttributes( + // With prompt caching, `inputTokens` leaves out cache reads and writes, which the conventions count as input. + typeof usage?.inputTokens === 'number' + ? usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) + : undefined, + usage?.outputTokens, + usage?.cacheWriteInputTokens, + usage?.cacheReadInputTokens, + ), + ); } private _responseHookConverseStream(response: NormalizedResponse, span: Span): void { + if (!response.data?.stream) { + span.end(); + return; + } + // Wrap and replace the response stream in place to process events into telemetry // before yielding to the user. - response.data.stream = wrapConverseStreamResponse(response.data.stream, span); + response.data.stream = wrapConverseStreamResponse(response.data.stream, span, response.request.abortSignal); } private _responseHookInvokeModel(response: NormalizedResponse, span: Span): void { @@ -276,7 +296,7 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, responseBody.results[0].tokenCount); } if (responseBody.results?.[0]?.completionReason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.results[0].completionReason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([responseBody.results[0].completionReason])); } } else if (currentModelId.includes('amazon.nova')) { if (responseBody.usage !== undefined) { @@ -288,17 +308,23 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { } } if (responseBody.stopReason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.stopReason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([responseBody.stopReason])); } } else if (currentModelId.includes('anthropic.claude')) { - if (responseBody.usage?.input_tokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, responseBody.usage.input_tokens); - } - if (responseBody.usage?.output_tokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, responseBody.usage.output_tokens); - } + const usage = responseBody.usage; + span.setAttributes( + getTokenUsageAttributes( + // Anthropic's `input_tokens` leaves out cache reads and writes, which the conventions count as input. + typeof usage?.input_tokens === 'number' + ? usage.input_tokens + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + : undefined, + usage?.output_tokens, + usage?.cache_creation_input_tokens, + usage?.cache_read_input_tokens, + ), + ); if (responseBody.stop_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.stop_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([responseBody.stop_reason])); } } else if (currentModelId.includes('meta.llama')) { if (responseBody.prompt_token_count !== undefined) { @@ -308,28 +334,31 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, responseBody.generation_token_count); } if (responseBody.stop_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.stop_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([responseBody.stop_reason])); } } else if (currentModelId.includes('cohere.command-r')) { if (responseBody.text !== undefined) { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, Math.ceil(responseBody.text.length / 6)); } if (responseBody.finish_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.finish_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([responseBody.finish_reason])); } } else if (currentModelId.includes('cohere.command')) { if (responseBody.generations?.[0]?.text !== undefined) { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, Math.ceil(responseBody.generations[0].text.length / 6)); } if (responseBody.generations?.[0]?.finish_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.generations[0].finish_reason]); + span.setAttribute( + GEN_AI_RESPONSE_FINISH_REASONS, + JSON.stringify([responseBody.generations[0].finish_reason]), + ); } } else if (currentModelId.includes('mistral')) { if (responseBody.outputs?.[0]?.text !== undefined) { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, Math.ceil(responseBody.outputs[0].text.length / 6)); } if (responseBody.outputs?.[0]?.stop_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [responseBody.outputs[0].stop_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([responseBody.outputs[0].stop_reason])); } } } @@ -338,13 +367,15 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { private _responseHookInvokeModelWithResponseStream(response: NormalizedResponse, span: Span): void { const stream = response.data?.body; const modelId = response.request.commandInput?.modelId; - if (!stream || !modelId) { + const abortSignal = response.request.abortSignal; + if (!stream) { + span.end(); return; } // Resolved once: the model family is fixed for the whole stream, and for unrecognized families // the chunks don't need to be parsed at all. - const recordAttributes = resolveStreamRecorder(modelId); + const recordAttributes = typeof modelId === 'string' ? resolveStreamRecorder(modelId) : undefined; // Replace the original response body with our instrumented stream, deferring span.end() until the // entire stream is consumed. Downstream consumers still receive the full stream. @@ -359,6 +390,9 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { } yield chunk; } + } catch (error) { + setStreamErrorStatus(span, abortSignal); + throw error; } finally { span.end(); } @@ -380,6 +414,7 @@ function resolveStreamRecorder(modelId: string): ((parsedChunk: ParsedChunk, spa async function* wrapConverseStreamResponse( stream: AsyncIterable, span: Span, + abortSignal: AbortSignal | undefined, ): AsyncGenerator { try { let usage: TokenUsage | undefined; @@ -388,27 +423,63 @@ async function* wrapConverseStreamResponse( usage = item.metadata?.usage; yield item; } - setUsage(span, usage); + span.setAttributes( + getTokenUsageAttributes( + // With prompt caching, `inputTokens` leaves out cache reads and writes, which the conventions count as input. + typeof usage?.inputTokens === 'number' + ? usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) + : undefined, + usage?.outputTokens, + usage?.cacheWriteInputTokens, + usage?.cacheReadInputTokens, + ), + ); + } catch (error) { + setStreamErrorStatus(span, abortSignal); + throw error; } finally { span.end(); } } +function setStreamErrorStatus(span: Span, abortSignal: AbortSignal | undefined): void { + span.setStatus({ code: SPAN_STATUS_ERROR, message: abortSignal?.aborted ? 'cancelled' : 'internal_error' }); +} + function setStopReason(span: Span, stopReason: string | undefined): void { if (stopReason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [stopReason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([stopReason])); } } -function setUsage(span: Span, usage: TokenUsage | undefined): void { - if (usage) { - const { inputTokens, outputTokens } = usage; - if (inputTokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, inputTokens); - } - if (outputTokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, outputTokens); - } +/** + * `getTokenUsageAttributes` is not used here: `message_start` carries only a placeholder output token count, + * so a total computed from it would be wrong. + */ +function setClaudeStreamInputTokens(span: Span, usage: ParsedChunk): void { + const cacheReadTokens = usage?.cache_read_input_tokens; + const cacheWriteTokens = usage?.cache_creation_input_tokens; + if (typeof usage?.input_tokens === 'number') { + span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, usage.input_tokens + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0)); + } + if (typeof cacheReadTokens === 'number') { + span.setAttribute(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cacheReadTokens); + } + if (typeof cacheWriteTokens === 'number') { + span.setAttribute(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, cacheWriteTokens); + } +} + +function parseRequestBody(body: unknown): ParsedChunk { + // `InvokeModel` takes the body as a JSON string or as bytes. + const text = typeof body === 'string' ? body : body instanceof Uint8Array ? textDecoder.decode(body) : undefined; + if (!text) { + return undefined; + } + try { + return JSON.parse(text); + } catch { + return undefined; } } @@ -435,19 +506,18 @@ function recordNovaAttributes(parsedChunk: ParsedChunk, span: Span): void { } } if (parsedChunk.messageStop?.stopReason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.messageStop.stopReason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.messageStop.stopReason])); } } function recordClaudeAttributes(parsedChunk: ParsedChunk, span: Span): void { - if (parsedChunk.message?.usage?.input_tokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, parsedChunk.message.usage.input_tokens); - } - if (parsedChunk.message?.usage?.output_tokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, parsedChunk.message.usage.output_tokens); + setClaudeStreamInputTokens(span, parsedChunk.message?.usage); + // `message_start` carries only a placeholder output count. The final count arrives in `message_delta`. + if (parsedChunk.usage?.output_tokens !== undefined) { + span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, parsedChunk.usage.output_tokens); } if (parsedChunk.delta?.stop_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.delta.stop_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.delta.stop_reason])); } } @@ -459,7 +529,7 @@ function recordTitanAttributes(parsedChunk: ParsedChunk, span: Span): void { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, parsedChunk.totalOutputTextTokenCount); } if (parsedChunk.completionReason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.completionReason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.completionReason])); } } @@ -471,33 +541,41 @@ function recordLlamaAttributes(parsedChunk: ParsedChunk, span: Span): void { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, parsedChunk.generation_token_count); } if (parsedChunk.stop_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.stop_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.stop_reason])); } } -function recordMistralAttributes(parsedChunk: ParsedChunk, span: Span): void { - if (parsedChunk.outputs?.[0]?.text !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, Math.ceil(parsedChunk.outputs[0].text.length / 6)); +/** + * Bedrock adds the token counts to the last chunk of a stream. For models that send no counts of their own, + * this is the only real count. + */ +function recordInvocationMetrics(parsedChunk: ParsedChunk, span: Span): void { + const metrics = parsedChunk['amazon-bedrock-invocationMetrics']; + if (typeof metrics?.inputTokenCount === 'number') { + span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS, metrics.inputTokenCount); } + if (typeof metrics?.outputTokenCount === 'number') { + span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, metrics.outputTokenCount); + } +} + +function recordMistralAttributes(parsedChunk: ParsedChunk, span: Span): void { + recordInvocationMetrics(parsedChunk, span); if (parsedChunk.outputs?.[0]?.stop_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.outputs[0].stop_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.outputs[0].stop_reason])); } } function recordCohereAttributes(parsedChunk: ParsedChunk, span: Span): void { - if (parsedChunk.generations?.[0]?.text !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, Math.ceil(parsedChunk.generations[0].text.length / 6)); - } + recordInvocationMetrics(parsedChunk, span); if (parsedChunk.generations?.[0]?.finish_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.generations[0].finish_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.generations[0].finish_reason])); } } function recordCohereRAttributes(parsedChunk: ParsedChunk, span: Span): void { - if (parsedChunk.text !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, Math.ceil(parsedChunk.text.length / 6)); - } + recordInvocationMetrics(parsedChunk, span); if (parsedChunk.finish_reason !== undefined) { - span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [parsedChunk.finish_reason]); + span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([parsedChunk.finish_reason])); } } diff --git a/packages/server-utils/src/integrations/aws-sdk/types.ts b/packages/server-utils/src/integrations/aws-sdk/types.ts index da6e978cfc45..206b10242e50 100644 --- a/packages/server-utils/src/integrations/aws-sdk/types.ts +++ b/packages/server-utils/src/integrations/aws-sdk/types.ts @@ -11,6 +11,8 @@ export interface NormalizedRequest { commandName: string; commandInput: CommandInput; region?: string; + /** The `abortSignal` the caller passed to `send`, if any. */ + abortSignal?: AbortSignal; } export interface NormalizedResponse { diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index 846ba9ae40cf..c26b37da75e9 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -63,7 +63,7 @@ function instrumentGoogleGenai(options: GoogleGenAIOptions): void { ); } }, - deferSpanEnd: ({ span, data }) => wrapStreamResult(span, data, options), + deferSpanEnd: ({ span, data }) => wrapStreamResult(span, data, operation, options), }, ); } @@ -136,7 +136,12 @@ function isAsyncIterable(value: unknown): value is AsyncIterableStream { * For a stream we patch `result[Symbol.asyncIterator]` in place so `instrumentStream` ends the * span when iteration finishes. */ -function wrapStreamResult(span: Span, data: OrchestrionChannelContext, options: GoogleGenAIOptions): boolean { +function wrapStreamResult( + span: Span, + data: OrchestrionChannelContext, + operation: string, + options: GoogleGenAIOptions, +): boolean { const result = data.result; if (!isAsyncIterable(result)) { return false; @@ -144,7 +149,13 @@ function wrapStreamResult(span: Span, data: OrchestrionChannelContext, options: const { recordOutputs } = resolveAIRecordingOptions(options); const iterate = result[Symbol.asyncIterator].bind(result); - const instrumented = instrumentStream({ [Symbol.asyncIterator]: iterate }, span, recordOutputs ?? false); + const params = resolveChatParams(operation, data.arguments?.[0] as Record | undefined, data.self); + const instrumented = instrumentStream( + { [Symbol.asyncIterator]: iterate }, + span, + recordOutputs ?? false, + (params?.config as { abortSignal?: AbortSignal } | undefined)?.abortSignal, + ); result[Symbol.asyncIterator] = () => instrumented; return true; diff --git a/packages/server-utils/src/integrations/hono/constants.ts b/packages/server-utils/src/integrations/hono/constants.ts index 297f3a469271..fff246e8bfce 100644 --- a/packages/server-utils/src/integrations/hono/constants.ts +++ b/packages/server-utils/src/integrations/hono/constants.ts @@ -2,3 +2,9 @@ // Shared by the `app.request` Proxy (`patchAppRequest`) and the orchestrion channel subscriber // (`instrumentInternalRequests` in `honoIntegration`), which both open this span. export const INTERNAL_REQUEST_ORIGIN = 'auto.http.hono.internal_request'; + +// Marks the Sentry request/response middleware so the span-wrapping patches never turn it into a +// middleware span — most importantly when an auto-instrumented sub-app carrying this middleware is +// mounted into a parent and `wrapSubAppMiddleware` wraps its handlers. Read by `wrapMiddlewareWithSpan` +// and `resolveRouteName`. +export const SENTRY_HONO_MIDDLEWARE = '__SENTRY_HONO_MIDDLEWARE__'; diff --git a/packages/server-utils/src/integrations/hono/createHonoMiddleware.ts b/packages/server-utils/src/integrations/hono/createHonoMiddleware.ts index 2540fc059871..1c5087cf567e 100644 --- a/packages/server-utils/src/integrations/hono/createHonoMiddleware.ts +++ b/packages/server-utils/src/integrations/hono/createHonoMiddleware.ts @@ -1,14 +1,10 @@ import { addNonEnumerableProperty, getDefaultIsolationScope, getIsolationScope } from '@sentry/core'; import type { HonoRuntime } from './connInfo'; +import { SENTRY_HONO_MIDDLEWARE } from './constants'; import type { Context, MiddlewareHandler } from './honoTypes'; import { captureContextError, requestHandler, responseHandler } from './middlewareHandlers'; import type { SentryHonoMiddlewareOptions } from './types'; -// Marks the Sentry request/response middleware so the span-wrapping patches never turn it into a -// middleware span — most importantly when an auto-instrumented sub-app carrying this middleware is -// mounted into a parent and `wrapSubAppMiddleware` wraps its handlers. Read by `wrapMiddlewareWithSpan`. -export const SENTRY_HONO_MIDDLEWARE = '__SENTRY_HONO_MIDDLEWARE__'; - // `Symbol.for` (global registry) so the markers are shared even if this module is evaluated from more // than one copy of `@sentry/server-utils`. const HONO_REQUEST_HANDLED = Symbol.for('sentry.hono.requestHandled'); diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index d5ef285cdf62..b97d2b13e307 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -10,11 +10,12 @@ import { CHANNELS } from '../../orchestrion/channels'; import { honoModuleNames } from '../../orchestrion/config/hono'; import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation'; import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-channel'; +import { isBun, isCloudflareWorkers, isDeno } from '../../utils/runtime'; import { applyPatches } from './applyPatches'; import { INTERNAL_REQUEST_ORIGIN } from './constants'; import { createHonoRequestMiddleware } from './createHonoMiddleware'; import { getMiddlewareFlags } from './isMiddleware'; -import { isMatchedHandlerList } from './matchResult'; +import { isMatchedHandlerList, wrapRouteHandlers } from './matchResult'; import { extractPathname, isInternalRequestSpanActive } from './patchAppRequest'; import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan'; import type { SentryHonoMiddlewareOptions } from './types'; @@ -25,12 +26,9 @@ const INTEGRATION_NAME = 'Hono' as const; export interface HonoIntegrationOptions extends SentryHonoMiddlewareOptions {} -const globalAny = globalThis as { Bun?: unknown; Deno?: unknown; navigator?: { userAgent?: string } }; -const isBun = typeof globalAny.Bun !== 'undefined'; -const isDeno = typeof globalAny.Deno !== 'undefined'; -const isCloudflare = globalAny.navigator?.userAgent === 'Cloudflare-Workers'; - -const runtime: HonoRuntime = isBun ? 'bun' : isDeno ? 'deno' : isCloudflare ? 'cloudflare' : 'node'; +function getHonoRuntime(): HonoRuntime { + return isBun() ? 'bun' : isDeno() ? 'deno' : isCloudflareWorkers() ? 'cloudflare' : 'node'; +} /** * Manually instruments a Hono app and returns the Sentry request/response middleware to register @@ -45,13 +43,12 @@ export function honoMiddleware(app: Hono, options: HonoIntegra applyPatches(app); return createHonoRequestMiddleware({ - runtime, + runtime: getHonoRuntime(), shouldHandleError: options.shouldHandleError, }); } -function wrapMatchedMiddleware(handlers: MatchedHandler[]): void { - const middlewareFlags = getMiddlewareFlags(handlers.map(([[, route]]) => route)); +function wrapMatchedMiddleware(handlers: MatchedHandler[], middlewareFlags: boolean[]): void { for (const [i, [pair]] of handlers.entries()) { if (middlewareFlags[i]) { pair[0] = wrapMiddlewareWithSpan(pair[0]); @@ -63,7 +60,9 @@ function wrapMatchedMiddleware(handlers: MatchedHandler[]): void { * Per-request Context hook. `#dispatch` builds `new Context(req, { matchResult })` before its * single-handler fast-path check, so from the live `matchResult` we: * 1. wrap the matched middleware handlers for spans (route handlers are covered by the request span); - * 2. prepend the Sentry request/response middleware — it drives route naming, request data and error + * 2. wrap the matched route handlers if more than one matched, so the route name follows the handler + * that responds; + * 3. prepend the Sentry request/response middleware — it drives route naming, request data and error * capture from inside the chain, and forces the ≥2-handler `compose` path so the fast-path is * never taken. * @@ -82,7 +81,9 @@ function injectHonoInstrumentation( injectedHandlerLists.add(handlers); if (isMatchedHandlerList(handlers)) { - wrapMatchedMiddleware(handlers); + const middlewareFlags = getMiddlewareFlags(handlers.map(([[, route]]) => route)); + wrapMatchedMiddleware(handlers, middlewareFlags); + wrapRouteHandlers(handlers, middlewareFlags); } // Prepend the Sentry request/response middleware. Its arity marks it as a middleware, so its `/*` @@ -138,7 +139,7 @@ function instrumentHono(options: HonoIntegrationOptions): void { // The request/response middleware is stateless (per-request state lives on the request scope) and // `options` is fixed, so build it once and reuse it across every dispatched Context. const requestMiddleware = createHonoRequestMiddleware({ - runtime, + runtime: getHonoRuntime(), shouldHandleError: options.shouldHandleError, }); diff --git a/packages/server-utils/src/integrations/hono/matchResult.ts b/packages/server-utils/src/integrations/hono/matchResult.ts index e6bd298f1206..aa6b847906c9 100644 --- a/packages/server-utils/src/integrations/hono/matchResult.ts +++ b/packages/server-utils/src/integrations/hono/matchResult.ts @@ -1,5 +1,16 @@ -import { isPlainObject } from '@sentry/core'; -import type { MatchedHandler } from './honoTypes'; +import { addNonEnumerableProperty, isPlainObject } from '@sentry/core'; +import type { Context, HonoRoute, MatchedHandler } from './honoTypes'; +import { sentryOriginalTraps } from './sentryOriginalTraps'; + +// `routeIndex` of the route handler that started last. `Symbol.for` shares it between SDK copies. +const ROUTE_HANDLER_INDEX = Symbol.for('sentry.hono.routeHandlerIndex'); + +// Marks the index wrapper. The router reuses the match result for each request to a route, so without +// it the wrappers pile up. A span wrapper does not count: a route handler from `app.use()` has one and +// still needs this wrapper. +const RECORDS_ROUTE_HANDLER_INDEX = Symbol.for('sentry.hono.recordsRouteHandlerIndex'); + +const ON_ROUTE_HANDLER_START = Symbol.for('sentry.hono.onRouteHandlerStart'); function isMatchedHandler(entry: unknown): entry is MatchedHandler { const pair: unknown = Array.isArray(entry) ? entry[0] : undefined; @@ -17,10 +28,90 @@ function isMatchedHandler(entry: unknown): entry is MatchedHandler { ); } -/** - * Checks the entry shape that `compose` and `hono/route` read. The match result is a Hono internal, so - * the SDK leaves other shapes alone. - */ +/** The match result is internal to Hono. The SDK uses it only in the shape that `compose` reads. */ export function isMatchedHandlerList(handlers: unknown): handlers is MatchedHandler[] { return Array.isArray(handlers) && handlers.every(isMatchedHandler); } + +/** + * Reads the match result like `hono/route` does (Hono 4.8+). Hono does not export the getter's symbol, + * so this looks for a symbol getter that returns a match result. + * https://github.com/honojs/hono/blob/v5.0.0-rc.1/src/helper/route/index.ts#L32-L34 + */ +export function getMatchedHandlers(context: Context): MatchedHandler[] | undefined { + const requestProto: object | null = Object.getPrototypeOf(context.req); + if (!requestProto) { + return undefined; + } + + for (const key of Object.getOwnPropertySymbols(requestProto)) { + if (typeof Object.getOwnPropertyDescriptor(requestProto, key)?.get !== 'function') { + continue; + } + const matchResult: unknown = Reflect.get(context.req, key); + if (Array.isArray(matchResult) && isMatchedHandlerList(matchResult[0])) { + return matchResult[0]; + } + } + return undefined; +} + +/** Runs `callback` each time a route handler of this request starts. */ +export function onRouteHandlerStart(context: Context, callback: () => void): void { + addNonEnumerableProperty(context, ON_ROUTE_HANDLER_START, callback); +} + +/** + * Wraps the route handlers so that each stores `c.req.routeIndex` when it starts. Hono 5 resets the + * index after `next()` (https://github.com/honojs/hono/pull/4424). + * + * Wraps only if more than one route handler matched. A single one is the handler that responds. Arity + * cannot tell if a handler calls `next()`: it does not count rest parameters or a default `next`. + * Returns `true` if it wrapped. + * + * Safe to call during the request: `compose` reads each handler only when it calls it. + */ +export function wrapRouteHandlers(handlers: MatchedHandler[], middlewareFlags: boolean[]): boolean { + const routeHandlerCount = middlewareFlags.filter(flag => !flag).length; + if (routeHandlerCount < 2) { + return false; + } + + for (const [i, [pair]] of handlers.entries()) { + if (!middlewareFlags[i]) { + pair[0] = recordRouteHandlerIndex(pair[0]); + } + } + return true; +} + +function recordRouteHandlerIndex(handler: HonoRoute['handler']): HonoRoute['handler'] { + if (Reflect.get(handler, RECORDS_ROUTE_HANDLER_INDEX)) { + return handler; + } + + const originalTraps = sentryOriginalTraps(handler); + return new Proxy(handler, { + apply(target, thisArg, args: [Context, ...unknown[]]) { + const [context] = args; + addNonEnumerableProperty(context, ROUTE_HANDLER_INDEX, context.req.routeIndex); + const onStart: unknown = Reflect.get(context, ON_ROUTE_HANDLER_START); + if (typeof onStart === 'function') { + onStart(); + } + return Reflect.apply(target, thisArg, args); + }, + ...originalTraps, + get(target, prop, receiver) { + if (prop === RECORDS_ROUTE_HANDLER_INDEX) { + return true; + } + return originalTraps.get?.(target, prop, receiver); + }, + }); +} + +export function getRecordedRouteHandlerIndex(context: Context): number | undefined { + const index: unknown = Reflect.get(context, ROUTE_HANDLER_INDEX); + return typeof index === 'number' ? index : undefined; +} diff --git a/packages/server-utils/src/integrations/hono/middlewareHandlers.ts b/packages/server-utils/src/integrations/hono/middlewareHandlers.ts index 6ca799b3e340..520e11ee9d38 100644 --- a/packages/server-utils/src/integrations/hono/middlewareHandlers.ts +++ b/packages/server-utils/src/integrations/hono/middlewareHandlers.ts @@ -8,6 +8,7 @@ import { INTERNAL_setSegmentNameSourceIfSegment, updateSpanName, type Scope, + type Span, type SpanAttributes, spanToJSON, winterCGHeadersToDict, @@ -18,6 +19,8 @@ import type { Context } from './honoTypes'; import { type HonoRuntime, readConnInfo } from './connInfo'; import { hasFetchEvent } from './hono-context'; import { defaultShouldHandleError } from './defaultShouldHandleError'; +import { getMiddlewareFlags } from './isMiddleware'; +import { getMatchedHandlers, onRouteHandlerStart, wrapRouteHandlers } from './matchResult'; import { resolveRouteName } from './resolveRouteName'; import { type SentryHonoMiddlewareOptions } from './types'; import { @@ -35,10 +38,24 @@ import { */ export function requestHandler(context: Context, runtime: HonoRuntime): void { const isolationScope = getCurrentIsolationScope(); - - // Set a provisional route name as early as possible so events captured during the request already carry the route. - // It is re-resolved in `responseHandler` once the middleware chain has run. - updateSpanRouteName(isolationScope, context); + // Read now: when a route handler starts, a user span can be active. + const span = getActiveSpan(); + + // Hono 5 removes `c.req.matchedRoutes`. Before Hono 4.8, it is the only source. + const matchedHandlers = getMatchedHandlers(context); + const routes = matchedHandlers?.map(([[, route]]) => route) ?? context.req.matchedRoutes ?? []; + const middlewareFlags = getMiddlewareFlags(routes); + const nameSpan = (): void => + updateSpanRouteName(isolationScope, span, context, resolveRouteName(context, routes, middlewareFlags)); + + // Name early, so that events from the handlers carry the route. + nameSpan(); + + // A route handler that calls `next()` passes the request on, so rename when each one starts. Without + // the getter, only the Context hook can wrap the route handlers. + if (!matchedHandlers || wrapRouteHandlers(matchedHandlers, middlewareFlags)) { + onRouteHandlerStart(context, nameSpan); + } isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(hasFetchEvent(context) ? context.event.request : context.req.raw), @@ -116,17 +133,13 @@ export function responseHandler( context: Context, shouldHandleError?: SentryHonoMiddlewareOptions['shouldHandleError'], ): void { - // Overwrite the route name now that the middleware chain has run: `routeIndex` is accurate here - updateSpanRouteName(getCurrentIsolationScope(), context); - captureContextError(context, shouldHandleError); } /** * Captures an unhandled context error, gated by the user's `shouldHandleError` (or the default). * - * Split out from {@link responseHandler} so a deduplicated middleware can still report its own - * context's error without re-resolving the route name or overwriting request data — e.g. the inner + * A deduplicated middleware calls it directly to report the error of its own context — e.g. the inner * context of an internal `.request()` whose route threw but whose failed response the outer handler * swallowed. */ @@ -143,10 +156,18 @@ export function captureContextError( } } -function updateSpanRouteName(isolationScope: Scope, context: Context): void { - const route = resolveRouteName(context); +function updateSpanRouteName( + isolationScope: Scope, + activeSpan: Span | undefined, + context: Context, + route: string | undefined, +): void { + // Without a route, keep the span name from the runtime. + if (route === undefined) { + return; + } + const routeName = `${context.req.method} ${route}`; - const activeSpan = getActiveSpan(); if (activeSpan) { activeSpan.updateName(routeName); diff --git a/packages/server-utils/src/integrations/hono/patchAppRequest.ts b/packages/server-utils/src/integrations/hono/patchAppRequest.ts index 15bb20cc21fd..d77dc4ef1d8a 100644 --- a/packages/server-utils/src/integrations/hono/patchAppRequest.ts +++ b/packages/server-utils/src/integrations/hono/patchAppRequest.ts @@ -4,6 +4,7 @@ import { debug, getActiveSpan, getOriginalFunction, startSpan, type WrappedFunct import { INTERNAL_REQUEST_ORIGIN } from './constants'; import type { Env, Hono } from './honoTypes'; import { DEBUG_BUILD } from '../../debug-build'; +import { sentryOriginalTraps } from './sentryOriginalTraps'; const INTERNAL_REQUEST_OP = HTTP_SERVER; @@ -113,12 +114,7 @@ export function patchAppRequest(app: Hono): void { }, ); }, - get(target, prop, receiver) { - if (prop === '__sentry_original__') { - return originalRequest; - } - return Reflect.get(target, prop, receiver); - }, + ...sentryOriginalTraps(originalRequest), }); DEBUG_BUILD && debug.log('[hono] Patched app.request for internal dispatch tracing.'); diff --git a/packages/server-utils/src/integrations/hono/patchRoute.ts b/packages/server-utils/src/integrations/hono/patchRoute.ts index 398df72fb924..b63add8f582a 100644 --- a/packages/server-utils/src/integrations/hono/patchRoute.ts +++ b/packages/server-utils/src/integrations/hono/patchRoute.ts @@ -5,6 +5,7 @@ import type { Hono, HonoRoute } from './honoTypes'; import { getMiddlewareFlags } from './isMiddleware'; import { patchAppRequest } from './patchAppRequest'; import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan'; +import { sentryOriginalTraps } from './sentryOriginalTraps'; export type { HonoRoute }; @@ -88,12 +89,7 @@ export function installRouteHookOnPrototype(honoBaseProto: HonoBaseProto): Route return Reflect.apply(_target, thisArg, args); }, - get(target, prop, receiver) { - if (prop === '__sentry_original__') { - return originalRoute; - } - return Reflect.get(target, prop, receiver); - }, + ...sentryOriginalTraps(originalRoute), }); honoBaseProto.__sentryRouteHook__ = handle; diff --git a/packages/server-utils/src/integrations/hono/resolveRouteName.ts b/packages/server-utils/src/integrations/hono/resolveRouteName.ts index 97b1283722ed..2f96bf83e278 100644 --- a/packages/server-utils/src/integrations/hono/resolveRouteName.ts +++ b/packages/server-utils/src/integrations/hono/resolveRouteName.ts @@ -1,50 +1,40 @@ import type { Context, HonoRoute } from './honoTypes'; -import { isMiddleware } from './isMiddleware'; - -// Arity alone is enough here (unlike `wrapSubAppMiddleware` in patchRoute.ts, which also needs position) -// We only want the path, and inline middleware shares its handler's path. -function isRouteHandler(handler: unknown): boolean { - return typeof handler === 'function' && !isMiddleware(handler); -} - -// Read the request's own matched routes rather than importing the `hono/route` helpers, so the -// instrumentation needs no runtime import from `hono`. `c.req.matchedRoutes` and `c.req.routePath` -// are the (deprecated but public) getters those helpers wrap; `routePath(c, index)` is reimplemented -// here as `matchedRoutes(c).at(index)?.path` so an arbitrary index (e.g. -1) can be resolved. -function matchedRoutes(context: Context): HonoRoute[] { - return context.req.matchedRoutes ?? []; -} - -function routePath(context: Context, index?: number): string { - const routes = matchedRoutes(context); - return routes.at(index ?? context.req.routeIndex)?.path ?? ''; -} +import { SENTRY_HONO_MIDDLEWARE } from './constants'; +import { getRecordedRouteHandlerIndex } from './matchResult'; /** - * Resolves the route path of the matched handler for the transaction name. - * - * Picking the handler (not just `routePath`) avoids two failure modes: a catch-all middleware - * registered after the handlers (`routePath(c, -1)` would return just `/*`), and a middleware that - * short-circuits before the handler runs (`routePath(c)` would return the middleware's path). + * Returns the path of the route that handles the request, or `undefined` if the request has no route. + * Not the last matched route: a catch-all registered later also matches. */ -export function resolveRouteName(context: Context): string { - const routes = matchedRoutes(context); +export function resolveRouteName( + context: Context, + routes: HonoRoute[], + middlewareFlags: boolean[], +): string | undefined { + const recordedIndex = getRecordedRouteHandlerIndex(context); + const recorded = recordedIndex === undefined ? undefined : routes[recordedIndex]; + if (recorded) { + return recorded.path; + } - // Trust routeIndex when it lands on a handler - to disambiguate overlapping handlers. - const current = routes[context.req.routeIndex]; - if (current && isRouteHandler(current.handler)) { - return current.path; + // Hono runs the first matched route handler first. If a middleware returned early, it is the one that + // would have run. + const firstRouteHandler = routes.find((_route, i) => !middlewareFlags[i]); + if (firstRouteHandler) { + return firstRouteHandler.path; } - // A middleware short-circuited, so routeIndex is stuck on it: fall back to the last matched handler. + // Only middleware matched, for example `app.use('/static/*', serveStatic())`. The Sentry middleware + // and `/*` middleware say nothing about the route. for (let i = routes.length - 1; i >= 0; i--) { const route = routes[i]; - if (route && isRouteHandler(route.handler)) { + if (route && route.path !== '/*' && !isSentryMiddleware(route.handler)) { return route.path; } } + return undefined; +} - // No handler matched (middleware-only path) - // Final matched route: https://hono.dev/docs/helpers/route#using-with-index-parameter - return routePath(context, -1); +function isSentryMiddleware(handler: unknown): boolean { + return typeof handler === 'function' && Boolean(Reflect.get(handler, SENTRY_HONO_MIDDLEWARE)); } diff --git a/packages/server-utils/src/integrations/hono/sentryOriginalTraps.ts b/packages/server-utils/src/integrations/hono/sentryOriginalTraps.ts new file mode 100644 index 000000000000..d623a0aa3819 --- /dev/null +++ b/packages/server-utils/src/integrations/hono/sentryOriginalTraps.ts @@ -0,0 +1,26 @@ +// oxlint-disable-next-line typescript/no-unsafe-function-type +type AnyFunction = Function; + +/** + * Proxy traps that expose `original` as `__sentry_original__` on a wrapping Proxy without defining it on the target, + * which is the original itself. `functionToStringIntegration` only unwraps own properties, so the property has to + * look like an own one too. + */ +export function sentryOriginalTraps( + original: AnyFunction, +): Pick, 'get' | 'getOwnPropertyDescriptor'> { + return { + get(target, prop, receiver) { + if (prop === '__sentry_original__') { + return original; + } + return Reflect.get(target, prop, receiver); + }, + getOwnPropertyDescriptor(target, prop) { + if (prop === '__sentry_original__') { + return { value: original, writable: true, enumerable: false, configurable: true }; + } + return Reflect.getOwnPropertyDescriptor(target, prop); + }, + }; +} diff --git a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts index 86f96e05039c..5ba3a80bafd4 100644 --- a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts +++ b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts @@ -9,8 +9,9 @@ import { type WrappedFunction, } from '@sentry/core'; import { type MiddlewareHandler } from './honoTypes'; -import { SENTRY_HONO_MIDDLEWARE } from './createHonoMiddleware'; +import { SENTRY_HONO_MIDDLEWARE } from './constants'; import { defaultShouldHandleError } from './defaultShouldHandleError'; +import { sentryOriginalTraps } from './sentryOriginalTraps'; const MIDDLEWARE_ORIGIN = 'auto.middleware.hono'; @@ -59,11 +60,6 @@ export function wrapMiddlewareWithSpan(handler: MiddlewareHandler): MiddlewareHa span.end(); } }, - get(target, prop, receiver) { - if (prop === '__sentry_original__') { - return handler; - } - return Reflect.get(target, prop, receiver); - }, + ...sentryOriginalTraps(handler), }); } diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index d9d5bd863144..ad94021c8677 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -3,9 +3,9 @@ import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration, isObjectLike } from '@sentry/core'; import { resolveAIRecordingOptions } from '../ai/core/utils'; import { createLangChainCallbackHandler } from '../ai/langchain'; -import { instrumentCompiledGraphInvoke } from '../ai/langgraph'; +import { instrumentCompiledGraph, isInsideManualLangGraphInstrumentation } from '../ai/langgraph'; import { LANGGRAPH_INTEGRATION_NAME } from '../ai/langgraph/constants'; -import type { CompiledGraph, LangGraphOptions } from '../ai/langgraph/types'; +import type { LangGraphOptions } from '../ai/langgraph/types'; import { extractAgentNameFromParams, extractLLMFromParams, wrapToolsWithSpans } from '../ai/langgraph/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { OrchestrionChannelContext } from '../orchestrion/types'; @@ -17,8 +17,8 @@ import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation // deduplicated out of the default set. const INTEGRATION_NAME = LANGGRAPH_INTEGRATION_NAME; -// `createReactAgent` compiles a `StateGraph` internally. When set, the compile subscriber skips that -// nested graph so its `invoke` is wrapped once (by the createReactAgent handler), not twice. +// `createReactAgent` compiles a `StateGraph` internally. When set, the compile subscriber skips that nested +// graph, so the createReactAgent handler wraps it with the agent's name and model instead. let insideCreateReactAgent = false; const _langGraphIntegration = ((options: LangGraphOptions = {}) => { @@ -38,19 +38,19 @@ function instrumentLanggraph(options: LangGraphOptions): void { const resolvedOptions = resolveAIRecordingOptions(options); const sentryHandler = createLangChainCallbackHandler(resolvedOptions); - // StateGraph.compile returns synchronously; wrap the returned graph's `invoke` at `end`. + // StateGraph.compile returns synchronously; wrap the returned graph's `invoke` and `stream` at `end`. diagnosticsChannel .tracingChannel(CHANNELS.LANGGRAPH_STATE_GRAPH_COMPILE) .end.subscribe(message => { - if (insideCreateReactAgent) { + if (insideCreateReactAgent || isInsideManualLangGraphInstrumentation()) { return; } const { arguments: args, result } = message as OrchestrionChannelContext; - wrapCompiledGraphInvoke(result, getFirstArgObject(args) ?? {}, resolvedOptions, null, sentryHandler); + instrumentCompiledGraph(result, getFirstArgObject(args) ?? {}, resolvedOptions, null, sentryHandler); }); - // createReactAgent only wraps tools and the returned graph's `invoke`. Tools are wrapped at - // `start` (before the agent runs), invoke at `end`. + // createReactAgent only wraps tools and the returned graph's `invoke` and `stream`. Tools are wrapped at + // `start` (before the agent runs), the graph at `end`. const reactAgentChannel = diagnosticsChannel.tracingChannel( CHANNELS.LANGGRAPH_CREATE_REACT_AGENT, ); @@ -72,10 +72,13 @@ function instrumentLanggraph(options: LangGraphOptions): void { }); reactAgentChannel.end.subscribe(message => { insideCreateReactAgent = false; + if (isInsideManualLangGraphInstrumentation()) { + return; + } const { arguments: args, result } = message as OrchestrionChannelContext; const agentName = extractAgentNameFromParams(args) ?? undefined; const compileOptions = agentName ? { name: agentName } : {}; - wrapCompiledGraphInvoke(result, compileOptions, resolvedOptions, extractLLMFromParams(args), sentryHandler); + instrumentCompiledGraph(result, compileOptions, resolvedOptions, extractLLMFromParams(args), sentryHandler); }); // Make sure a thrown `createReactAgent` doesn't leave the suppression flag stuck on. reactAgentChannel.error.subscribe(() => { @@ -89,35 +92,6 @@ function getFirstArgObject(args: unknown[] | undefined): Record return isObjectLike(first) ? first : undefined; } -/** - * Wrap the compiled graph's `invoke` with the shared `invoke_agent` instrumentation, exactly as the - * OTel path does on the returned graph. - */ -function wrapCompiledGraphInvoke( - graph: unknown, - compileOptions: Record, - options: LangGraphOptions, - llm: ReturnType, - sentryHandler: unknown, -): void { - if (!graph || typeof graph !== 'object') { - return; - } - - const compiledGraph = graph as CompiledGraph; - const originalInvoke = compiledGraph.invoke; - if (typeof originalInvoke === 'function') { - compiledGraph.invoke = instrumentCompiledGraphInvoke( - originalInvoke.bind(compiledGraph), - compiledGraph, - compileOptions, - options, - llm, - sentryHandler, - ); - } -} - /** * Diagnostics-channel-based LangGraph integration. Subscribes to the diagnostics_channels * injected into `@langchain/langgraph`'s `StateGraph.compile` and `createReactAgent`, so it requires diff --git a/packages/server-utils/src/integrations/nitro/captureTracingEvents.ts b/packages/server-utils/src/integrations/nitro/captureTracingEvents.ts index 800acc5b30ca..938590229708 100644 --- a/packages/server-utils/src/integrations/nitro/captureTracingEvents.ts +++ b/packages/server-utils/src/integrations/nitro/captureTracingEvents.ts @@ -301,7 +301,7 @@ function setParameterizedRouteAttributes(span: Span, event: NitroH3Event): void if (isObjectLike(params)) { Object.entries(params).forEach(([key, value]) => { - // Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey + // Based on this convention: https://conventions.sentry.dev/attributes/url/#url-path-parameter-key rootSpan.setAttributes({ [`url.path.parameter.${key}`]: String(value), [`params.${key}`]: String(value), diff --git a/packages/server-utils/src/integrations/serveTunnel.ts b/packages/server-utils/src/integrations/serveTunnel.ts new file mode 100644 index 000000000000..345192b2e3dc --- /dev/null +++ b/packages/server-utils/src/integrations/serveTunnel.ts @@ -0,0 +1,46 @@ +import type { ServerTunnelOptions } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; +import { + _INTERNAL_handleServedNodeTunnelRequest as handleServedNodeTunnelRequest, + _INTERNAL_setNodeTunnelHandler as setNodeTunnelHandler, + HTTP_ON_SERVER_REQUEST, + serveTunnelIntegration as coreServeTunnelIntegration, +} from '@sentry/core/server'; +import { subscribe } from '../utils/diagnosticsChannel'; +import { instrumentTunnelServer } from '../utils/tunnelServer'; + +let isSubscribed = false; + +/** + * Lets every `node:http` server answer managed tunnel requests before the app sees them, whatever framework runs on + * it. Nothing is answered unless the tunnel is on, through `serveTunnelIntegration` or a framework SDK. + */ +export function instrumentTunnelServers(): void { + if (isSubscribed) { + return; + } + + isSubscribed = true; + // Core's HTTP server instrumentation answers tunnel requests through it, without bundling it for apps that have no + // tunnel. + setNodeTunnelHandler(handleServedNodeTunnelRequest); + subscribe(HTTP_ON_SERVER_REQUEST, instrumentTunnelServer); +} + +/** + * Makes a `serveTunnelIntegration` that, unless `intercept` is off, also runs `instrumentRuntimeServers` to answer + * tunnel requests on the runtime's own servers, like `Bun.serve` or `Deno.serve`. + */ +export function makeServeTunnelIntegration(instrumentRuntimeServers?: () => void) { + return defineIntegration((options: ServerTunnelOptions = {}) => { + return { + ...coreServeTunnelIntegration(options), + setupOnce() { + if (options.intercept !== false) { + instrumentTunnelServers(); + instrumentRuntimeServers?.(); + } + }, + }; + }); +} diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index c1daaf73fc84..e707b9299676 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -26,6 +26,8 @@ import { GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, SENTRY_ORIGIN, + VERCEL_AI_SETTINGS_CONTEXT_KEY_BASE, + VERCEL_AI_TOOL_CALL_CONTEXT_KEY_BASE, } from '@sentry/conventions/attributes'; import { GEN_AI_DECIDE, @@ -70,6 +72,9 @@ const ORIGIN = 'auto.ai.vercelai'; // `gen_ai.operation.name` values, keyed to the span op they map to. const GEN_AI_OPERATION_SPAN_OPS = { embeddings: GEN_AI_EMBEDDINGS, + // `GEN_AI_RERANK` is deprecated because the product doesn't support rerank spans, but we keep + // emitting it so existing rerank spans keep their op. + // oxlint-disable-next-line typescript/no-deprecated rerank: GEN_AI_RERANK, decide: GEN_AI_DECIDE, invoke_agent: GEN_AI_INVOKE_AGENT, @@ -86,8 +91,6 @@ const VERCEL_AI_OPERATION_ID_ATTRIBUTE = 'vercel.ai.operationId'; const VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE = 'vercel.ai.model.provider'; const VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE = 'vercel.ai.settings.maxRetries'; const VERCEL_AI_TELEMETRY_METADATA_ATTRIBUTE_PREFIX = 'vercel.ai.telemetry.metadata.'; -const VERCEL_AI_SETTINGS_CONTEXT_ATTRIBUTE_PREFIX = 'vercel.ai.settings.context.'; -const VERCEL_AI_TOOL_CALL_CONTEXT_ATTRIBUTE_PREFIX = 'vercel.ai.tool_call.context.'; // Tracks the top-level operationId (and whether it streams) per `callId` so a model-call span can // name its `doGenerate`/`doStream` operation the same way the OTel integration does. `isStream` is @@ -459,7 +462,7 @@ export function createSpanFromMessage( const runtimeContextAttributes = (callId ? runtimeContextAttributesByCallId.get(callId) : undefined) ?? - contextAttributes(event, event.runtimeContext, VERCEL_AI_SETTINGS_CONTEXT_ATTRIBUTE_PREFIX); + contextAttributes(event, event.runtimeContext, VERCEL_AI_SETTINGS_CONTEXT_KEY_BASE); if (callId && ROOT_OPERATION_TYPES.has(type)) { runtimeContextAttributesByCallId.set(callId, runtimeContextAttributes); } @@ -541,7 +544,7 @@ function telemetryMetadataAttributes(metadata: unknown): Record * looks the same as an unfiltered one. `aiVersion` (added in 7.0.154) is the only reliable sign that * the AI SDK filtered it, so without it we record nothing. */ -function contextAttributes(event: Record, context: unknown, prefix: string): SpanAttributes { +function contextAttributes(event: Record, context: unknown, keyBase: string): SpanAttributes { const attributes: SpanAttributes = {}; if (typeof event.aiVersion !== 'string' || !isObjectLike(context)) { return attributes; @@ -549,7 +552,7 @@ function contextAttributes(event: Record, context: unknown, pre for (const [key, value] of Object.entries(context)) { const attributeValue = stringify(value); if (value !== null && attributeValue !== undefined) { - attributes[`${prefix}${key}`] = attributeValue; + attributes[`${keyBase}.${key}`] = attributeValue; } } return attributes; @@ -629,7 +632,7 @@ function buildToolSpan( return startGenAiSpan('execute_tool', toolName, { [SENTRY_ORIGIN]: ORIGIN, ...runtimeContextAttributes, - ...contextAttributes(event, event.toolContext, VERCEL_AI_TOOL_CALL_CONTEXT_ATTRIBUTE_PREFIX), + ...contextAttributes(event, event.toolContext, VERCEL_AI_TOOL_CALL_CONTEXT_KEY_BASE), [GEN_AI_TOOL_NAME]: toolName, [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: toolCallId, [GEN_AI_TOOL_DESCRIPTION]: description, @@ -945,15 +948,24 @@ function resolveRecording( return globalDefault === true; } +/** The `ai` SDK accepts the system prompt as `string | SystemModelMessage | SystemModelMessage[]`. */ +function getSystemInstructionParts(instructions: unknown): { type: 'text'; content: string }[] { + const messages = Array.isArray(instructions) ? instructions : [instructions]; + return messages.flatMap(message => { + const content = asString(isObjectLike(message) ? message.content : message); + return content ? [{ type: 'text' as const, content }] : []; + }); +} + function buildInputMessageAttributes(event: Record): Record { const attributes: Record = {}; // `ai` >= 7 forbids system messages in `messages`/`prompt` and exposes the system prompt as a // separate `instructions` field. The OTel path lifts the system message out of the prompt into // `gen_ai.system_instructions` as `[{ type: 'text', content }]`; mirror that shape here. - const instructions = asString(event.instructions); - if (instructions) { - attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = stringify([{ type: 'text', content: instructions }]); + const instructions = getSystemInstructionParts(event.instructions); + if (instructions.length) { + attributes[GEN_AI_SYSTEM_INSTRUCTIONS] = stringify(instructions); } // The AI SDK start events extend `StandardizedPrompt`; messages live on `messages`, otherwise the diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index 2a59de79b4f8..2947af526c01 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -666,7 +666,7 @@ function buildTextMessage(type: 'generateText' | 'streamText' | 'generateObject' // The `ai` SDK takes the system prompt as a top-level `system` option (all of v4/v5/v6); the // shared core lifts `event.instructions` into the system-instructions attribute, matching v7's // native channel (which carries it as a distinct field rather than inside the messages array). - instructions: asString(options.system), + instructions: options.system, // Normalize to the message-array shape the shared core (and v7's channel) expects: a bare string // `prompt` becomes a single user message, matching the SDK's own normalization. messages: normalizePromptMessages(options), diff --git a/packages/server-utils/src/orchestrion/detect.ts b/packages/server-utils/src/orchestrion/detect.ts index 572afea7b5f5..f388648e4c16 100644 --- a/packages/server-utils/src/orchestrion/detect.ts +++ b/packages/server-utils/src/orchestrion/detect.ts @@ -1,4 +1,5 @@ -import { debug, GLOBAL_OBJ } from '@sentry/core'; +import { consoleSandbox, debug, GLOBAL_OBJ } from '@sentry/core'; +import { isBun } from '../utils/runtime'; /** * The module names (e.g. `mysql`, `@hapi/hapi`) orchestrion has already injected @@ -32,9 +33,25 @@ export function getOrchestrionInjectedModules(): string[] { * is fine. */ export function detectOrchestrionSetup(): void { - const { runtime, bundler } = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {}; + const marker = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {}; + const { runtime, bundler, channelIntegrations } = marker; if (!runtime && !bundler) { + if (channelIntegrations && isBun()) { + if (!marker.warnedBunRunNotInjected) { + marker.warnedBunRunNotInjected = true; + consoleSandbox(() => { + // oxlint-disable-next-line no-console + console.warn( + '[Sentry] No diagnostics-channel injection detected. Channel-based integrations will not record ' + + 'spans or capture errors, because Bun cannot instrument modules at runtime. Build your app with ' + + '`@sentry/bun/plugin` to enable them.', + ); + }); + } + return; + } + debug.warn( '[Sentry] No diagnostics-channel injection detected. Channel-based integrations ' + 'will not record spans. Make sure the diagnostics channels are injected ' + diff --git a/packages/server-utils/src/orchestrion/instrumentation.ts b/packages/server-utils/src/orchestrion/instrumentation.ts index 0dbddefc4da7..53b523eae2a5 100644 --- a/packages/server-utils/src/orchestrion/instrumentation.ts +++ b/packages/server-utils/src/orchestrion/instrumentation.ts @@ -2,6 +2,7 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { Client } from '@sentry/core'; import { addNonEnumerableProperty, debug, GLOBAL_OBJ, waitForTracingChannelBinding } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; +import { isBun, isCloudflareWorkers, isDeno } from '../utils/runtime'; import { getOrchestrionInjectedModules } from './detect'; // Marks an instrumentation callback as already run, so it subscribes at most once @@ -13,10 +14,6 @@ const INSTRUMENTED = Symbol.for('SentryOrchestrionInstrumented'); // oxlint-disable-next-line typescript/no-explicit-any type InstrumentationFn = ((...args: any[]) => void) & { [INSTRUMENTED]?: boolean }; -const globalAny = globalThis as { Bun?: unknown; Deno?: unknown; navigator?: { userAgent?: string } }; -const isBun = typeof globalAny.Bun !== 'undefined'; -const isDeno = typeof globalAny.Deno !== 'undefined'; - // Labels already warned about, so a client per request does not repeat the warning. const warnedMissingCloudflarePlugin = new Set(); @@ -58,6 +55,8 @@ export function invokeOrchestrionInstrumentation(); const tracingChannelsByName = new Map(); diff --git a/packages/server-utils/src/utils/runtime.ts b/packages/server-utils/src/utils/runtime.ts new file mode 100644 index 000000000000..eb01caa69d0d --- /dev/null +++ b/packages/server-utils/src/utils/runtime.ts @@ -0,0 +1,18 @@ +import { GLOBAL_OBJ } from '@sentry/core'; + +const globalWithRuntimes = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Bun?: unknown; Deno?: unknown }; + +/** Whether the SDK runs in Bun. */ +export function isBun(): boolean { + return typeof globalWithRuntimes.Bun !== 'undefined'; +} + +/** Whether the SDK runs in Deno. */ +export function isDeno(): boolean { + return typeof globalWithRuntimes.Deno !== 'undefined'; +} + +/** Whether the SDK runs in Cloudflare Workers. */ +export function isCloudflareWorkers(): boolean { + return GLOBAL_OBJ.navigator?.userAgent === 'Cloudflare-Workers'; +} diff --git a/packages/server-utils/src/utils/tunnelServer.ts b/packages/server-utils/src/utils/tunnelServer.ts new file mode 100644 index 000000000000..6cda5779396f --- /dev/null +++ b/packages/server-utils/src/utils/tunnelServer.ts @@ -0,0 +1,41 @@ +import { debug } from '@sentry/core'; +import { + _INTERNAL_handleServedNodeTunnelRequest as handleServedNodeTunnelRequest, + _INTERNAL_httpServerHandlesTunnel as httpServerHandlesTunnel, +} from '@sentry/core/server'; +import { DEBUG_BUILD } from '../debug-build'; + +type TunnelServer = { emit(event: string, ...data: unknown[]): boolean }; + +const tunnelServers = new WeakSet(); + +/** + * Wraps a Node server's `emit` once, so requests to the tunnel `serveTunnelIntegration` serves are answered before + * any of the app's `request` listeners run. A listener for the `http.server.request.start` diagnostics channel, which + * Node publishes right before it emits each request, so even the first request is answered. Only without core's HTTP + * server instrumentation, whose own `emit` wrapper answers them otherwise. + * + * It never throws, since Node rethrows a channel listener's error as an uncaught exception. + */ +export function instrumentTunnelServer(message: unknown): void { + try { + const server = (message as { server?: TunnelServer } | undefined)?.server; + if (!server || tunnelServers.has(server) || httpServerHandlesTunnel()) { + return; + } + + tunnelServers.add(server); + // oxlint-disable-next-line typescript/unbound-method -- called with the server as `this` below + const originalEmit = server.emit; + server.emit = function (this: TunnelServer, event: string, ...data: unknown[]) { + const [request, response] = data as Parameters; + if (event === 'request' && handleServedNodeTunnelRequest(request, response)) { + return true; + } + + return originalEmit.call(this, event, ...data); + }; + } catch (error) { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to instrument a server', error); + } +} diff --git a/packages/server-utils/src/utils/wrapServerRequestHandler.ts b/packages/server-utils/src/utils/wrapServerRequestHandler.ts index e519141b0de4..fec8f40b7f78 100644 --- a/packages/server-utils/src/utils/wrapServerRequestHandler.ts +++ b/packages/server-utils/src/utils/wrapServerRequestHandler.ts @@ -31,6 +31,7 @@ import { } from '@sentry/core'; import type { ServerRequestOptions } from '@sentry/core/server'; import { + _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, classifyResponseStreaming, getClientIPAddress, isStaticAssetRequest, @@ -83,6 +84,7 @@ export async function wrapServerRequestHandler R | Promise, ): Promise { const { request, client, isolationScope, options, mechanism } = config; + const method = request.method.toUpperCase(); const captureError = (error: unknown): void => { if (mechanism) { @@ -90,6 +92,30 @@ export async function wrapServerRequestHandler { const tool = this.tools.find(t => t.name === name)!; @@ -47,6 +77,28 @@ class FakeSessionRunner { return { isError: true }; } } + public async *[Symbol.asyncIterator](): AsyncGenerator<{ name: string; isError: boolean }> { + if (this._consumed) { + throw new Error('Cannot iterate over a consumed SessionToolRunner'); + } + this._consumed = true; + const loop = (async () => { + const results = []; + for (const { name, ...toolUse } of this._calls) { + await new Promise(resolve => setTimeout(resolve, 1)); + results.push({ name, ...(await this.dispatch(name, toolUse)) }); + } + return results; + })(); + try { + for (const result of await loop) { + yield result; + } + } finally { + await new Promise(resolve => setTimeout(resolve, 1)); + this.drained = true; + } + } } function fakeClient() { @@ -54,7 +106,9 @@ function fakeClient() { beta: { sessions: { events: { - toolRunner: vi.fn((_sessionId: string, opts: { tools: Tool[] }) => new FakeSessionRunner(opts)), + toolRunner: vi.fn( + (sessionId: string, opts: { tools: Tool[]; calls?: ToolCall[] }) => new FakeSessionRunner(sessionId, opts), + ), }, }, messages: { @@ -74,33 +128,41 @@ describe('instrumentAnthropicAiClient tool runner', () => { let endedSpans: Span[]; let events: ErrorEvent[]; - beforeEach(() => { - getMainCarrier().__SENTRY__ = undefined; - endedSpans = []; - events = []; + function initClient(options: { tracesSampleRate?: number } = {}): void { const client = new TestClient( getDefaultTestClientOptions({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1, + integrations: [conversationIdIntegration()], beforeSend: event => { events.push(event); return null; }, + ...options, }), ); setCurrentClient(client); client.init(); client.on('spanEnd', span => endedSpans.push(span)); + } + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + endedSpans = []; + events = []; + initClient({ tracesSampleRate: 1 }); }); afterEach(() => { getMainCarrier().__SENTRY__ = undefined; }); - function toolSpans() { - return endedSpans.map(span => spanToStaticSpanJSON(span)).filter(span => span.op === GEN_AI_EXECUTE_TOOL); + function spansWithOp(op: string) { + return endedSpans.map(span => spanToStaticSpanJSON(span)).filter(span => span.op === op); } + const toolSpans = () => spansWithOp(GEN_AI_EXECUTE_TOOL); + const agentSpans = () => spansWithOp(GEN_AI_INVOKE_AGENT); + it('wraps each tool call of the runner in an execute_tool span under the active span', async () => { const weather = tool('weather', async () => 'sunny'); const serverTool = { type: 'web_search_20250305', name: 'web_search' }; @@ -287,6 +349,11 @@ describe('instrumentAnthropicAiClient tool runner', () => { }); describe('sessions.events.toolRunner()', () => { + // The runner's tool calls run in a background loop, so their parent comes from the async context. + beforeEach(() => { + setAsyncLocalStorageAsyncContextStrategy(); + }); + it('wraps each tool call of the session runner in an execute_tool span', async () => { const weather = tool('weather', async () => 'sunny'); @@ -312,6 +379,297 @@ describe('instrumentAnthropicAiClient tool runner', () => { ]); }); + function defaultTools(): Tool[] { + return [ + tool('weather', async () => 'sunny'), + tool('read', async () => '# Weather app'), + tool('explode', async () => { + throw new Error('disk full'); + }), + ]; + } + + async function drain(runner: AsyncIterable): Promise { + for await (const _call of runner) { + } + } + + function sessionRunner(sessionId: string, calls: ToolCall[], tools = defaultTools()) { + return instrumentAnthropicAiClient(fakeClient()).beta.sessions.events.toolRunner(sessionId, { tools, calls }); + } + + it('parents the tool spans of each runner to its own invoke_agent span in its own trace, outside any span', async () => { + const first = sessionRunner('sesn_vienna', [ + { name: 'weather', id: 'sevt_1', input: { city: 'Vienna' } }, + { name: 'read', id: 'sevt_2', input: { file_path: 'README.md' } }, + ]); + const second = sessionRunner('sesn_graz', [{ name: 'weather', id: 'sevt_3', input: { city: 'Graz' } }]); + + await Promise.all([drain(first), drain(second)]); + + const agents = agentSpans(); + expect(agents).toHaveLength(2); + for (const agent of agents) { + expect(agent.parent_span_id).toBeUndefined(); + } + expect(agents[0]!.trace_id).not.toBe(agents[1]!.trace_id); + const vienna = agents.find(span => span.data[GEN_AI_CONVERSATION_ID] === 'sesn_vienna')!; + const graz = agents.find(span => span.data[GEN_AI_CONVERSATION_ID] === 'sesn_graz')!; + expect( + toolSpans() + .map(span => [span.data[GEN_AI_TOOL_CALL_ID_ATTRIBUTE], span.parent_span_id]) + .sort(), + ).toEqual([ + ['sevt_1', vienna.span_id], + ['sevt_2', vienna.span_id], + ['sevt_3', graz.span_id], + ]); + }); + + it('gives each tool span the session id of its runner, even for a tool shared between runners', async () => { + const tools = [tool('weather', async () => 'sunny')]; + + await Promise.all([ + drain(sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }], tools)), + drain(sessionRunner('sesn_graz', [{ name: 'weather', id: 'sevt_2', input: {} }], tools)), + ]); + + expect( + toolSpans() + .map(span => [span.data[GEN_AI_TOOL_CALL_ID_ATTRIBUTE], span.data[GEN_AI_CONVERSATION_ID]]) + .sort(), + ).toEqual([ + ['sevt_1', 'sesn_vienna'], + ['sevt_2', 'sesn_graz'], + ]); + }); + + it.each([ + [ + 'a scope', + (run: () => Promise) => + withScope(scope => { + scope.setConversationId('conv_support_42'); + return run(); + }), + ], + [ + 'Sentry.setConversationId()', + (run: () => Promise) => + withIsolationScope(() => { + setConversationId('conv_support_42'); + return run(); + }), + ], + ])('keeps a conversation id set on %s on the runner and tool spans', async (_name, withConversationId) => { + await withConversationId(() => + drain(sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }])), + ); + + expect(agentSpans()[0]!.data[GEN_AI_CONVERSATION_ID]).toBe('conv_support_42'); + expect(toolSpans()[0]!.data[GEN_AI_CONVERSATION_ID]).toBe('conv_support_42'); + }); + + it('gives the runner span the invoke_agent op, name and session id', async () => { + await drain(sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }])); + + expect(agentSpans()).toEqual([ + expect.objectContaining({ + description: 'invoke_agent', + origin: 'auto.ai.anthropic', + status: 'ok', + data: expect.objectContaining({ + [GEN_AI_OPERATION_NAME]: 'invoke_agent', + [GEN_AI_PROVIDER_NAME]: 'anthropic', + [GEN_AI_CONVERSATION_ID]: 'sesn_vienna', + }), + }), + ]); + }); + + it('nests the runner span under the active span', async () => { + const parent = await startSpan({ name: 'handle work' }, async span => { + await drain(sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }])); + return spanToStaticSpanJSON(span); + }); + + expect(agentSpans()[0]!.parent_span_id).toBe(parent.span_id); + expect(toolSpans()[0]!.parent_span_id).toBe(agentSpans()[0]!.span_id); + }); + + it('keeps the trace of an active span that is not sampled', async () => { + const runner = sessionRunner('sesn_vienna', [{ name: 'explode', id: 'sevt_1', input: {} }]); + const unsampled = new SentryNonRecordingSpan({ traceId: '4c3a5f2e8d9b1c7a6e5f4d3c2b1a0f9e' }); + + await withActiveSpan(unsampled, () => drain(runner)); + + await vi.waitFor(() => expect(events).toHaveLength(1)); + expect(events[0]!.contexts?.trace?.trace_id).toBe('4c3a5f2e8d9b1c7a6e5f4d3c2b1a0f9e'); + expect(agentSpans()).toEqual([]); + }); + + it('keeps the scope data of one session off the next outside any span', async () => { + const tagging = tool('tagging', async () => getIsolationScope().setTag('customer', 'acme')); + + await drain(sessionRunner('sesn_acme', [{ name: 'tagging', id: 'sevt_1', input: {} }], [tagging])); + await drain(sessionRunner('sesn_globex', [{ name: 'explode', id: 'sevt_2', input: {} }])); + + await vi.waitFor(() => expect(events).toHaveLength(1)); + expect(events[0]!.tags?.customer).toBeUndefined(); + }); + + it('keeps the scope and trace of a request that has no span', async () => { + const runner = sessionRunner('sesn_vienna', [{ name: 'explode', id: 'sevt_1', input: {} }]); + + const requestTraceId = await withIsolationScope(async scope => { + scope.setTag('request', 'POST /work'); + await drain(runner); + return getCurrentScope().getPropagationContext().traceId; + }); + + await vi.waitFor(() => expect(events).toHaveLength(1)); + expect(events[0]!.tags?.request).toBe('POST /work'); + expect(events[0]!.contexts?.trace?.trace_id).toBe(requestTraceId); + }); + + it('keeps a trace continued without a span', async () => { + await continueTrace( + { sentryTrace: '4c3a5f2e8d9b1c7a6e5f4d3c2b1a0f9e-8b5d36b773a00b34-1', baggage: undefined }, + () => drain(sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }])), + ); + + expect(agentSpans()[0]).toMatchObject({ + trace_id: '4c3a5f2e8d9b1c7a6e5f4d3c2b1a0f9e', + parent_span_id: '8b5d36b773a00b34', + }); + }); + + it('sends the trace of each session with its requests when tracing is off', async () => { + initClient(); + const sentTraceIds: string[] = []; + const explode = tool('explode', async () => { + sentTraceIds.push(getTraceData()['sentry-trace']!.split('-')[0]!); + throw new Error('disk full'); + }); + await Promise.all( + ['sesn_vienna', 'sesn_graz'].map(sessionId => + drain(sessionRunner(sessionId, [{ name: 'explode', id: `sevt_${sessionId}`, input: {} }], [explode])), + ), + ); + + await vi.waitFor(() => expect(events).toHaveLength(2)); + expect(new Set(sentTraceIds).size).toBe(2); + expect(events.map(event => event.contexts?.trace?.trace_id).sort()).toEqual([...sentTraceIds].sort()); + }); + + it('closes the runner when the consumer throws into the loop', async () => { + const runner = sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }]); + const iterator = runner[Symbol.asyncIterator](); + await iterator.next(); + + await expect(iterator.throw!(new Error('stop'))).rejects.toThrow('stop'); + + expect(runner.drained).toBe(true); + }); + + it('ends the runner span when closing the runner fails', async () => { + const runner = { + sessionId: 'sesn_vienna', + tools: [], + [Symbol.asyncIterator]: () => ({ + next: async () => ({ done: false, value: 'call' }), + return: async () => { + throw new Error('close failed'); + }, + }), + }; + instrumentSessionToolRunner(runner, {}); + + const leaveEarly = async (): Promise => { + for await (const _call of runner as AsyncIterable) { + break; + } + }; + + await expect(leaveEarly()).rejects.toThrow('close failed'); + expect(agentSpans()).toHaveLength(1); + }); + + it('ends the runner span as failed when the first next() throws synchronously', async () => { + const runner = { + sessionId: 'sesn_vienna', + tools: [], + [Symbol.asyncIterator]: () => ({ + next: () => { + throw new Error('not iterable'); + }, + }), + }; + instrumentSessionToolRunner(runner, {}); + + await expect(drain(runner as AsyncIterable)).rejects.toThrow('not iterable'); + expect(agentSpans().map(span => span.status)).toEqual(['internal_error']); + }); + + it('starts the runner span on the first next(), not when the iterator is created', async () => { + const started: string[] = []; + getClient()!.on('spanStart', span => started.push(spanToStaticSpanJSON(span).op!)); + const runner = sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }]); + + const iterator = runner[Symbol.asyncIterator](); + expect(started).toEqual([]); + + await iterator.next(); + expect(started).toEqual([GEN_AI_INVOKE_AGENT, GEN_AI_EXECUTE_TOOL]); + }); + + it('ends the runner span after the runner has drained when the loop is left early', async () => { + const runner = sessionRunner('sesn_vienna', [ + { name: 'weather', id: 'sevt_1', input: {} }, + { name: 'read', id: 'sevt_2', input: {} }, + ]); + + const drainedWhenSpanEnded: boolean[] = []; + getClient()!.on('spanEnd', span => { + if (spanToStaticSpanJSON(span).op === GEN_AI_INVOKE_AGENT) { + drainedWhenSpanEnded.push(runner.drained); + } + }); + + for await (const _call of runner) { + break; + } + + expect(drainedWhenSpanEnded).toEqual([true]); + }); + + it('marks the runner span as failed and rethrows when the runner is iterated twice', async () => { + const runner = sessionRunner('sesn_vienna', []); + await drain(runner); + + await expect(drain(runner)).rejects.toThrow('Cannot iterate over a consumed SessionToolRunner'); + + expect(agentSpans().map(span => span.status)).toEqual(['ok', 'internal_error']); + }); + + it('starts one runner span when the runner is instrumented twice', async () => { + const runner = sessionRunner('sesn_vienna', [{ name: 'weather', id: 'sevt_1', input: {} }]); + instrumentSessionToolRunner(runner, {}); + + await drain(runner); + + expect(agentSpans()).toHaveLength(1); + }); + + it('iterates a runner it cannot patch without a runner span', async () => { + const runner = Object.preventExtensions(new FakeSessionRunner('sesn_vienna', { tools: [], calls: [] })); + + instrumentSessionToolRunner(runner, {}); + await drain(runner); + + expect(agentSpans()).toEqual([]); + }); + it('captures the error of a failing tool, which the session runner turns into a result', async () => { const explode = tool('explode', async () => { throw new Error('disk full'); diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts index a8038425e158..660ea39a3f3c 100644 --- a/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts @@ -2,6 +2,7 @@ import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; import { describe, expect, it, vi } from 'vitest'; import type { LangChainMessage } from '../../../../src/ai/langchain/types'; import { + _INTERNAL_addLangChainCallbackHandler, _INTERNAL_mergeLangChainCallbackHandler, getConversationIdFromMetadata, getConversationIdMetadataFromConfig, @@ -68,6 +69,34 @@ describe('normalizeLangChainMessages', () => { }); }); +describe('_INTERNAL_addLangChainCallbackHandler', () => { + const sentryHandler = { name: 'SentryCallbackHandler' }; + const listener = { name: 'Listener' }; + + it('appends even when the sentry handler is already present', () => { + expect(_INTERNAL_addLangChainCallbackHandler([sentryHandler], listener)).toStrictEqual([sentryHandler, listener]); + expect(_INTERNAL_addLangChainCallbackHandler(sentryHandler, listener)).toStrictEqual([sentryHandler, listener]); + expect(_INTERNAL_addLangChainCallbackHandler(undefined, listener)).toStrictEqual([listener]); + }); + + it('adds to a copy of a callback manager that already holds the sentry handler', () => { + const manager = { + handlers: [sentryHandler] as unknown[], + addHandler: vi.fn(function (this: { handlers: unknown[] }, handler: unknown) { + this.handlers.push(handler); + }), + copy: vi.fn(() => ({ ...manager, handlers: [...manager.handlers] })), + }; + + const result = _INTERNAL_addLangChainCallbackHandler(manager, listener) as typeof manager; + + expect(result).not.toBe(manager); + expect(manager.handlers).toEqual([sentryHandler]); + expect(result.handlers).toEqual([sentryHandler, listener]); + expect(result.addHandler).toHaveBeenCalledWith(listener, true); + }); +}); + describe('_INTERNAL_mergeLangChainCallbackHandler', () => { const sentryHandler = { name: 'SentryCallbackHandler' }; diff --git a/packages/server-utils/test/ai/lib/tracing/langgraph-stream.test.ts b/packages/server-utils/test/ai/lib/tracing/langgraph-stream.test.ts new file mode 100644 index 000000000000..eaf7fc2f020c --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/langgraph-stream.test.ts @@ -0,0 +1,387 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON, startSpan } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { GEN_AI_RESPONSE_STREAMING, GEN_AI_RESPONSE_TEXT } from '@sentry/conventions/attributes'; +import { GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; +import { + instrumentCompiledGraph, + instrumentCreateReactAgent, + instrumentStateGraphCompile, +} from '../../../../src/ai/langgraph'; +import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; + +type Handler = Record void) | undefined>; +type Emit = (event: string, ...args: unknown[]) => void; + +// The parts of LangGraph's `IterableReadableStreamWithAbortSignal` the instrumentation relies on: the stream is +// its own async iterator, `return()` is what a `break` calls, and `cancel()` aborts `signal`. +function createStream(chunks: unknown[] = ['chunk']) { + const controller = new AbortController(); + let index = 0; + const stream = { + signal: controller.signal, + [Symbol.asyncIterator]() { + return stream; + }, + async next(): Promise> { + return index < chunks.length ? { done: false, value: chunks[index++] } : { done: true, value: undefined }; + }, + return: vi.fn(async (): Promise> => ({ done: true, value: undefined })), + cancel: vi.fn(async (): Promise => controller.abort()), + }; + return stream; +} + +function getHandlers(callbacks: unknown): Handler[] { + if (Array.isArray(callbacks)) { + return callbacks as Handler[]; + } + return ((callbacks as { handlers?: Handler[] } | undefined)?.handlers ?? []) as Handler[]; +} + +// A compiled graph whose `stream()` reports its root run to `config.callbacks` the way LangGraph does: the root +// run starts before `stream()` resolves, and ends or fails later. +function createGraph(run: (emit: Emit, input: unknown) => unknown) { + return { + async stream(input: unknown, config?: { callbacks?: unknown; metadata?: unknown }): Promise { + const emit: Emit = (event, ...args) => + getHandlers(config?.callbacks).forEach(handler => handler[event]?.(...args)); + emit('handleChainStart', {}, input, 'root', undefined, [], config?.metadata); + return run(emit, input); + }, + }; +} + +describe('LangGraph stream() agent span', () => { + let endedSpans: Span[]; + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + const client = new TestClient( + getDefaultTestClientOptions({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1 }), + ); + setCurrentClient(client); + client.init(); + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function instrument(graph: T, options = {}, sentryCallbackHandler?: unknown): T { + instrumentCompiledGraph(graph, { name: 'weather_graph' }, options, undefined, sentryCallbackHandler); + return graph; + } + + const agentSpans = (): Span[] => endedSpans.filter(span => spanToStaticSpanJSON(span).op === GEN_AI_INVOKE_AGENT); + + it('ends the span when the root run ends, not when a child run does', async () => { + let emit!: Emit; + const graph = instrument( + createGraph(e => { + emit = e; + return createStream(); + }), + { recordOutputs: true }, + ); + + await graph.stream({ messages: [{ role: 'user', content: 'Weather?' }] }); + emit('handleChainStart', {}, {}, 'child', 'root'); + emit('handleChainEnd', {}, 'child', 'root'); + expect(agentSpans()).toHaveLength(0); + + emit( + 'handleChainEnd', + { + messages: [ + { role: 'user', content: 'Weather?' }, + { role: 'assistant', content: 'Sunny' }, + ], + }, + 'root', + ); + + expect(agentSpans()).toHaveLength(1); + const span = spanToStaticSpanJSON(agentSpans()[0]!); + expect(span.description).toBe('invoke_agent weather_graph'); + expect(span.status).not.toBe('internal_error'); + expect(span.data[GEN_AI_RESPONSE_STREAMING]).toBe(true); + expect(span.data[GEN_AI_RESPONSE_TEXT]).toBe('[{"role":"assistant","content":"Sunny"}]'); + }); + + it('ends the span when the run finishes although the stream is never read', async () => { + const graph = instrument( + createGraph(emit => { + setTimeout(() => emit('handleChainEnd', {}, 'root')); + return createStream(); + }), + ); + + await graph.stream({}); + await vi.waitFor(() => expect(agentSpans()).toHaveLength(1)); + }); + + it('ends the span when the caller breaks out of the stream', async () => { + const stream = createStream(['first', 'second']); + const originalReturn = stream.return; + const graph = instrument(createGraph(() => stream)); + + for await (const _chunk of (await graph.stream({})) as AsyncIterable) { + break; + } + + expect(originalReturn).toHaveBeenCalledTimes(1); + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).status).not.toBe('internal_error'); + }); + + it('passes a rejection from the original return() through to the caller', async () => { + const error = new Error('return failed'); + const stream = createStream(); + stream.return.mockRejectedValueOnce(error); + const graph = instrument(createGraph(() => stream)); + + const consume = async (): Promise => { + for await (const _chunk of (await graph.stream({})) as AsyncIterable) { + break; + } + }; + + await expect(consume()).rejects.toBe(error); + expect(agentSpans()).toHaveLength(1); + }); + + it('treats a cancel by the caller as a finished run', async () => { + let emit!: Emit; + const graph = instrument( + createGraph(e => { + emit = e; + return createStream(); + }), + ); + + const stream = (await graph.stream({})) as ReturnType; + await stream.cancel(); + emit('handleChainError', new Error('Abort'), 'root'); + + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).status).not.toBe('internal_error'); + }); + + it('keeps the span open when cancel() is rejected, and still reports a later failure', async () => { + let emit!: Emit; + const graph = instrument( + createGraph(e => { + emit = e; + const stream = createStream(); + stream.cancel.mockRejectedValueOnce(new TypeError('locked')); + return stream; + }), + ); + + const stream = (await graph.stream({})) as ReturnType; + await expect(stream.cancel()).rejects.toThrow(TypeError); + expect(agentSpans()).toHaveLength(0); + + emit('handleChainError', new Error('node failed'), 'root'); + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).status).toBe('internal_error'); + }); + + it('does not mark an aborted run as failed when the caller aborted its own signal', async () => { + const controller = new AbortController(); + const graph = instrument( + createGraph(emit => { + controller.abort(); + emit('handleChainError', new Error('Abort'), 'root'); + return createStream(); + }), + ); + + await graph.stream({}, { signal: controller.signal }); + + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).status).not.toBe('internal_error'); + }); + + it('marks the span failed and rethrows when stream() rejects', async () => { + const error = new Error('bad input'); + const graph = instrument( + createGraph(() => { + throw error; + }), + ); + + await expect(graph.stream({})).rejects.toBe(error); + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).status).toBe('internal_error'); + }); + + it('ends the span right away when stream() reports no run to follow', async () => { + const graph = instrument({ stream: async () => createStream() }); + + await graph.stream(); + + expect(agentSpans()).toHaveLength(1); + }); + + it('still ends the span when recording the output fails', async () => { + let emit!: Emit; + const graph = instrument( + createGraph(e => { + emit = e; + return createStream(); + }), + { recordOutputs: true }, + ); + const outputs = { + get messages(): never { + throw new Error('unreadable state'); + }, + }; + + await graph.stream({}); + expect(() => emit('handleChainEnd', outputs, 'root')).not.toThrow(); + expect(agentSpans()).toHaveLength(1); + }); + + it('does not add the per-call listener to the config object the caller passed', async () => { + const callerHandler = { name: 'callerHandler' }; + const config = { callbacks: [callerHandler], metadata: { user: 'u' } }; + const graph = instrument(createGraph(() => createStream())); + + await graph.stream({}, config); + + expect(config).toEqual({ callbacks: [callerHandler], metadata: { user: 'u' } }); + }); + + it('copies a callback manager instead of adding the listener to it', async () => { + let received: unknown; + const manager = { + handlers: [] as unknown[], + addHandler(handler: unknown) { + this.handlers.push(handler); + }, + copy() { + return { ...manager, handlers: [...manager.handlers], addHandler: manager.addHandler, copy: manager.copy }; + }, + }; + const graph = instrument({ + async stream(_input: unknown, config: { callbacks: unknown }) { + received = config.callbacks; + return createStream(); + }, + }); + + await graph.stream({}, { callbacks: manager }); + + expect(manager.handlers).toHaveLength(0); + expect(received).not.toBe(manager); + expect((received as typeof manager).handlers).toHaveLength(1); + }); + + it('creates one span for invoke(), which streams internally', async () => { + const graph = instrument({ + ...createGraph(emit => { + emit('handleChainEnd', {}, 'root'); + return createStream(); + }), + async invoke(this: { stream: (input: unknown) => Promise }, input: unknown) { + for await (const _chunk of (await this.stream(input)) as AsyncIterable) { + // drain, as LangGraph's invoke does + } + return {}; + }, + }); + + await graph.invoke({}); + + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).data[GEN_AI_RESPONSE_STREAMING]).toBeUndefined(); + }); + + it('creates a child span for a stream() of the same graph started inside invoke()', async () => { + const graph = instrument({ + ...createGraph(emit => { + emit('handleChainEnd', {}, 'root'); + return createStream(); + }), + async invoke(this: { stream: (input: unknown) => Promise }, input: unknown) { + await this.stream(input); + await this.stream(input); + return {}; + }, + }); + + await graph.invoke({}); + + const [streamSpan, invokeSpan] = agentSpans(); + expect(agentSpans()).toHaveLength(2); + expect(spanToStaticSpanJSON(streamSpan!).parent_span_id).toBe(invokeSpan!.spanContext().spanId); + expect(spanToStaticSpanJSON(streamSpan!).data[GEN_AI_RESPONSE_STREAMING]).toBe(true); + }); + + it('wraps a graph only once when it is instrumented twice', async () => { + const graph = instrument( + instrument( + createGraph(emit => { + emit('handleChainEnd', {}, 'root'); + return createStream(); + }), + ), + ); + + await startSpan({ name: 'root' }, () => graph.stream({})); + + expect(agentSpans()).toHaveLength(1); + }); + + it('leaves the caller config untouched in invoke() and stream() when the Sentry handler is injected', async () => { + const graph = instrument( + { + ...createGraph(emit => { + emit('handleChainEnd', {}, 'root'); + return createStream(); + }), + async invoke( + this: { stream: (input: unknown, config: unknown) => Promise }, + input: unknown, + config: unknown, + ) { + await this.stream(input, config); + return {}; + }, + }, + {}, + { name: 'SentryCallbackHandler' }, + ); + const config = { configurable: { thread_id: 't1' } }; + + await graph.invoke({}, config); + await graph.stream({}, config); + + expect(config).toEqual({ configurable: { thread_id: 't1' } }); + }); + + it('names the span after the agent when createReactAgent compiles through an instrumented compile()', async () => { + const compile = instrumentStateGraphCompile( + () => + createGraph(emit => { + emit('handleChainEnd', {}, 'root'); + return createStream(); + }) as never, + {}, + ); + const createReactAgent = instrumentCreateReactAgent(() => compile()); + + const agent = createReactAgent({ name: 'weather_agent', llm: { modelName: 'mock-model' } }) as unknown as { + stream: (input: unknown) => Promise; + }; + await agent.stream({}); + + expect(agentSpans()).toHaveLength(1); + expect(spanToStaticSpanJSON(agentSpans()[0]!).description).toBe('invoke_agent weather_agent'); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/stream-errors.test.ts b/packages/server-utils/test/ai/lib/tracing/stream-errors.test.ts new file mode 100644 index 000000000000..d34dc3d4a1d3 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/stream-errors.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { Span } from '@sentry/core'; +import { SPAN_STATUS_ERROR } from '@sentry/core'; +import { GEN_AI_RESPONSE_STREAMING, GEN_AI_USAGE_INPUT_TOKENS } from '@sentry/conventions/attributes'; +import { instrumentAsyncIterableStream } from '../../../../src/ai/anthropic-ai/streaming'; +import { instrumentStream as instrumentGoogleGenAIStream } from '../../../../src/ai/google-genai/streaming'; +import type { GoogleGenAIResponse } from '../../../../src/ai/google-genai/types'; +import { instrumentStream as instrumentOpenAIStream } from '../../../../src/ai/openai/streaming'; + +function createMockSpan(): { span: Span; attributes: Record; setStatus: ReturnType } { + const attributes: Record = {}; + const setStatus = vi.fn(); + let isEnded = false; + const span = { + isRecording: () => !isEnded, + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + setAttributes: (attrs: Record) => { + Object.assign(attributes, attrs); + }, + setStatus, + end: () => { + isEnded = true; + }, + } as unknown as Span; + return { span, attributes, setStatus }; +} + +async function drain(stream: AsyncIterable): Promise { + for await (const _ of stream) { + void _; + } +} + +const providers = [ + { + name: 'openai', + instrument: instrumentOpenAIStream, + chunks: [ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'gpt-4o', + choices: [{ index: 0, delta: { content: 'The forecast for Berlin ' } }], + usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 }, + }, + { id: 'chatcmpl-1', object: 'chat.completion.chunk', choices: [{ index: 0, delta: { content: 'is ' } }] }, + ], + }, + { + name: 'anthropic', + instrument: instrumentAsyncIterableStream, + chunks: [ + { type: 'message_start', message: { id: 'msg_1', model: 'claude-sonnet-4-5', usage: { input_tokens: 8 } } }, + { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'The forecast for Berlin ' } }, + ], + }, + { + name: 'google-genai', + instrument: instrumentGoogleGenAIStream, + chunks: [ + { + candidates: [{ content: { parts: [{ text: 'The forecast for Berlin ' }], role: 'model' }, index: 0 }], + usageMetadata: { promptTokenCount: 8, candidatesTokenCount: 4, totalTokenCount: 12 }, + }, + { candidates: [{ content: { parts: [{ text: 'is ' }], role: 'model' }, index: 0 }] }, + ], + }, +] as const; + +describe.each(providers)('$name stream errors', ({ instrument, chunks }) => { + const run = (stream: AsyncIterable, span: Span) => + (instrument as (stream: AsyncIterable, span: Span, recordOutputs: boolean) => AsyncGenerator)( + stream, + span, + false, + ); + + it('marks the span as errored and keeps the partial usage when the stream throws midway', async () => { + const { span, attributes, setStatus } = createMockSpan(); + const networkError = new Error('terminated'); + async function* failingStream(): AsyncGenerator { + yield* chunks; + throw networkError; + } + + await expect(drain(run(failingStream(), span))).rejects.toBe(networkError); + + expect(setStatus).toHaveBeenCalledTimes(1); + expect(setStatus).toHaveBeenCalledWith({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + expect(span.isRecording()).toBe(false); + expect(attributes[GEN_AI_RESPONSE_STREAMING]).toBe(true); + expect(attributes[GEN_AI_USAGE_INPUT_TOKENS]).toBe(8); + }); + + it('leaves the span status alone when the consumer breaks out of the stream early', async () => { + const { span, attributes, setStatus } = createMockSpan(); + async function* endlessStream(): AsyncGenerator { + yield* chunks; + throw new Error('the consumer stopped before this point'); + } + + for await (const _ of run(endlessStream(), span)) { + break; + } + + expect(setStatus).not.toHaveBeenCalled(); + expect(span.isRecording()).toBe(false); + expect(attributes[GEN_AI_USAGE_INPUT_TOKENS]).toBe(8); + }); +}); + +describe('google-genai stream aborts', () => { + const abortError = new DOMException('This operation was aborted', 'AbortError'); + + async function* abortedStream(): AsyncGenerator { + yield {}; + throw abortError; + } + + it("marks the span as cancelled when the caller's signal fired", async () => { + const { span, setStatus } = createMockSpan(); + const controller = new AbortController(); + controller.abort(); + + await expect(drain(instrumentGoogleGenAIStream(abortedStream(), span, false, controller.signal))).rejects.toBe( + abortError, + ); + + expect(setStatus).toHaveBeenCalledTimes(1); + expect(setStatus).toHaveBeenCalledWith({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); + }); + + it("marks the span as errored when an abort didn't come from the caller's signal", async () => { + const { span, setStatus } = createMockSpan(); + + await expect( + drain(instrumentGoogleGenAIStream(abortedStream(), span, false, new AbortController().signal)), + ).rejects.toBe(abortError); + + expect(setStatus).toHaveBeenCalledTimes(1); + expect(setStatus).toHaveBeenCalledWith({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/utils/anthropic-utils.test.ts b/packages/server-utils/test/ai/lib/utils/anthropic-utils.test.ts index b7b974f870e3..591c5ae004a0 100644 --- a/packages/server-utils/test/ai/lib/utils/anthropic-utils.test.ts +++ b/packages/server-utils/test/ai/lib/utils/anthropic-utils.test.ts @@ -41,6 +41,21 @@ describe('anthropic-ai-utils', () => { ]); }); + it('includes the text of system blocks as one system message', () => { + expect( + messagesFromParams({ + messages: [{ role: 'user', content: 'hello' }], + system: [ + { type: 'text', text: 'Be brief.' }, + { type: 'text', text: 'Be kind.', cache_control: { type: 'ephemeral' } }, + ], + }), + ).toStrictEqual([ + { role: 'system', content: 'Be brief.\nBe kind.' }, + { role: 'user', content: 'hello' }, + ]); + }); + it('looks to params.input ahead of params.messages', () => { expect( messagesFromParams({ diff --git a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts new file mode 100644 index 000000000000..a63e70d3324c --- /dev/null +++ b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts @@ -0,0 +1,331 @@ +import { + GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REQUEST_MAX_TOKENS, + GEN_AI_REQUEST_MODEL, + GEN_AI_REQUEST_TEMPERATURE, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { GEN_AI_GENERATE_CONTENT } from '@sentry/conventions/op'; +import type { Span } from '@sentry/core'; +import { setCurrentClient, spanToJSON, startInactiveSpan } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { BedrockRuntimeServiceExtension } from '../../src/integrations/aws-sdk/services/bedrock-runtime'; +import type { NormalizedResponse, RequestMetadata } from '../../src/integrations/aws-sdk/types'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; + +const CLAUDE_MODEL_ID = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; + +const CLAUDE_REQUEST_BODY = { + anthropic_version: 'bedrock-2023-05-31', + max_tokens: 1024, + temperature: 0.5, + messages: [{ role: 'user', content: 'What is the capital of France?' }], +}; + +function invokeModelPreSpanHook(commandInput: Record): RequestMetadata { + return new BedrockRuntimeServiceExtension().requestPreSpanHook({ + serviceName: 'BedrockRuntime', + commandName: 'InvokeModel', + commandInput, + }); +} + +describe('BedrockRuntimeServiceExtension InvokeModel request', () => { + it('reads a request body passed as bytes', () => { + const body = new TextEncoder().encode(JSON.stringify(CLAUDE_REQUEST_BODY)); + + const { spanAttributes } = invokeModelPreSpanHook({ modelId: CLAUDE_MODEL_ID, body }); + + expect(spanAttributes).toEqual({ + [GEN_AI_PROVIDER_NAME]: 'aws.bedrock', + [GEN_AI_OPERATION_NAME]: 'generate_content', + [GEN_AI_REQUEST_MODEL]: CLAUDE_MODEL_ID, + [GEN_AI_REQUEST_MAX_TOKENS]: 1024, + [GEN_AI_REQUEST_TEMPERATURE]: 0.5, + }); + }); + + it('keeps the span when the request body is not JSON', () => { + const metadata = invokeModelPreSpanHook({ modelId: CLAUDE_MODEL_ID, body: 'Human: Hello\n\nAssistant:' }); + + expect(metadata).toEqual({ + spanName: `generate_content ${CLAUDE_MODEL_ID}`, + spanOp: GEN_AI_GENERATE_CONTENT, + isStream: false, + spanAttributes: { + [GEN_AI_PROVIDER_NAME]: 'aws.bedrock', + [GEN_AI_OPERATION_NAME]: 'generate_content', + [GEN_AI_REQUEST_MODEL]: CLAUDE_MODEL_ID, + }, + }); + }); + + it('keeps the span when the modelId is missing', () => { + const metadata = invokeModelPreSpanHook({ body: JSON.stringify(CLAUDE_REQUEST_BODY) }); + + expect(metadata).toEqual({ + spanName: 'generate_content', + spanOp: GEN_AI_GENERATE_CONTENT, + isStream: false, + spanAttributes: { + [GEN_AI_PROVIDER_NAME]: 'aws.bedrock', + [GEN_AI_OPERATION_NAME]: 'generate_content', + }, + }); + }); +}); + +const STREAM_COMMANDS = [ + { commandName: 'ConverseStream', streamField: 'stream' }, + { commandName: 'InvokeModelWithResponseStream', streamField: 'body' }, +]; + +function respond( + span: Span, + commandName: string, + commandInput: Record, + data: unknown, + abortSignal?: AbortSignal, +): void { + const response: NormalizedResponse = { + data, + request: { serviceName: 'BedrockRuntime', commandName, commandInput, abortSignal }, + requestId: 'b0a5e5a2-2c2b-4f56-9d0e-8c7c2d3f4a10', + }; + new BedrockRuntimeServiceExtension().responseHook(response, span); +} + +async function drain(stream: AsyncIterable): Promise { + for await (const _ of stream) { + // consume + } +} + +describe('BedrockRuntimeServiceExtension stream spans', () => { + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + setCurrentClient(client); + client.init(); + }); + + afterEach(() => { + setCurrentClient(undefined as never); + }); + + it.each( + STREAM_COMMANDS.flatMap(command => [ + { ...command, when: 'fails', abortSignal: undefined, status: 'error' }, + // `cancelled` is sent as `ok` under span streaming. + { ...command, when: 'is aborted by the caller', abortSignal: AbortSignal.abort(), status: 'ok' }, + ]), + )( + 'sets status $status when a $commandName stream $when', + async ({ commandName, streamField, abortSignal, status }) => { + const span = startInactiveSpan({ name: 'bedrock' }); + const error = new Error('ThrottlingException: Too many tokens, please wait before trying again.'); + const data: Record = { + [streamField]: (async function* () { + yield { messageStart: { role: 'assistant' } }; + throw error; + })(), + }; + + respond(span, commandName, { modelId: CLAUDE_MODEL_ID }, data, abortSignal); + + await expect(drain(data[streamField] as AsyncIterable)).rejects.toBe(error); + expect(spanToJSON(span).status).toBe(status); + expect(spanToJSON(span).end_timestamp).toBeDefined(); + }, + ); + + it.each(STREAM_COMMANDS)('ends a $commandName span when the response has no stream', ({ commandName }) => { + const span = startInactiveSpan({ name: 'bedrock' }); + + respond(span, commandName, { modelId: CLAUDE_MODEL_ID }, {}); + + expect(spanToJSON(span).end_timestamp).toBeDefined(); + }); + + it('reads the Claude stream output tokens from message_delta', async () => { + const span = startInactiveSpan({ name: 'bedrock' }); + const encoder = new TextEncoder(); + const chunks = [ + { type: 'message_start', message: { role: 'assistant', usage: { input_tokens: 14, output_tokens: 1 } } }, + { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 42 } }, + ]; + const data = { + body: (async function* () { + for (const chunk of chunks) { + yield { chunk: { bytes: encoder.encode(JSON.stringify(chunk)) } }; + } + })(), + }; + + respond(span, 'InvokeModelWithResponseStream', { modelId: CLAUDE_MODEL_ID }, data); + await drain(data.body); + + expect(spanToJSON(span).attributes?.[GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(42); + }); + + const INVOCATION_METRICS = { + inputTokenCount: 11, + outputTokenCount: 3, + invocationLatency: 412, + firstByteLatency: 198, + }; + + it.each( + [ + { modelId: 'mistral.mistral-large-2402-v1:0', toChunk: (text: string) => ({ outputs: [{ text }] }) }, + { modelId: 'cohere.command-text-v14', toChunk: (text: string) => ({ generations: [{ text }] }) }, + { modelId: 'cohere.command-r-plus-v1:0', toChunk: (text: string) => ({ text }) }, + ].flatMap(family => [ + { + ...family, + when: 'has invocation metrics', + metrics: INVOCATION_METRICS, + expected: { [GEN_AI_USAGE_INPUT_TOKENS]: 11, [GEN_AI_USAGE_OUTPUT_TOKENS]: 3 }, + }, + { + ...family, + when: 'has no invocation metrics', + metrics: undefined, + expected: { [GEN_AI_USAGE_INPUT_TOKENS]: undefined, [GEN_AI_USAGE_OUTPUT_TOKENS]: undefined }, + }, + ]), + )('records only real token counts when a $modelId stream $when', async ({ modelId, toChunk, metrics, expected }) => { + const span = startInactiveSpan({ name: 'bedrock' }); + const encoder = new TextEncoder(); + const chunks = [toChunk('The capital'), { ...toChunk(' is Paris.'), 'amazon-bedrock-invocationMetrics': metrics }]; + const data = { + body: (async function* () { + for (const chunk of chunks) { + yield { chunk: { bytes: encoder.encode(JSON.stringify(chunk)) } }; + } + })(), + }; + + respond(span, 'InvokeModelWithResponseStream', { modelId }, data); + await drain(data.body); + + const attributes = spanToJSON(span).attributes; + expect({ + [GEN_AI_USAGE_INPUT_TOKENS]: attributes?.[GEN_AI_USAGE_INPUT_TOKENS], + [GEN_AI_USAGE_OUTPUT_TOKENS]: attributes?.[GEN_AI_USAGE_OUTPUT_TOKENS], + }).toEqual(expected); + }); + + it('ends an InvokeModelWithResponseStream span without a modelId when the stream is drained', async () => { + const span = startInactiveSpan({ name: 'bedrock' }); + const data = { body: (async function* () {})() }; + + respond(span, 'InvokeModelWithResponseStream', {}, data); + await drain(data.body); + + expect(spanToJSON(span).end_timestamp).toBeDefined(); + }); +}); + +describe('BedrockRuntimeServiceExtension token usage with prompt caching', () => { + const encoder = new TextEncoder(); + + // 20 uncached prompt tokens, 1800 read from the cache, and 300 written to it. + const EXPECTED_INPUT_USAGE = { + [GEN_AI_USAGE_INPUT_TOKENS]: 2120, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 1800, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 300, + }; + + const EXPECTED_USAGE = { + ...EXPECTED_INPUT_USAGE, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 50, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2170, + }; + + const CLAUDE_USAGE = { + input_tokens: 20, + cache_read_input_tokens: 1800, + cache_creation_input_tokens: 300, + output_tokens: 50, + }; + + function usageAttributes(span: Span, expected: Record): Record { + const attributes = spanToJSON(span).attributes ?? {}; + return Object.fromEntries(Object.keys(expected).map(key => [key, attributes[key]])); + } + + beforeEach(() => { + const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 })); + setCurrentClient(client); + client.init(); + }); + + afterEach(() => { + setCurrentClient(undefined as never); + }); + + it('counts cache reads and writes as input tokens for Converse', () => { + const span = startInactiveSpan({ name: 'bedrock' }); + + respond( + span, + 'Converse', + { modelId: CLAUDE_MODEL_ID }, + { + stopReason: 'end_turn', + usage: { + inputTokens: 20, + outputTokens: 50, + totalTokens: 2170, + cacheReadInputTokens: 1800, + cacheWriteInputTokens: 300, + }, + }, + ); + + expect(usageAttributes(span, EXPECTED_USAGE)).toEqual(EXPECTED_USAGE); + }); + + it('counts cache reads and writes as input tokens for Claude InvokeModel', () => { + const span = startInactiveSpan({ name: 'bedrock' }); + const body = encoder.encode( + JSON.stringify({ + id: 'msg_bdrk_01Fv2yWBqLQ8Pp4q9kJbDxXz', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'Paris.' }], + stop_reason: 'end_turn', + usage: CLAUDE_USAGE, + }), + ); + + respond(span, 'InvokeModel', { modelId: CLAUDE_MODEL_ID }, { body }); + + expect(usageAttributes(span, EXPECTED_USAGE)).toEqual(EXPECTED_USAGE); + }); + + it('counts cache reads and writes as input tokens for a Claude stream', async () => { + const span = startInactiveSpan({ name: 'bedrock' }); + const messageStart = { + type: 'message_start', + message: { id: 'msg_bdrk_01Fv2yWBqLQ8Pp4q9kJbDxXz', role: 'assistant', usage: CLAUDE_USAGE }, + }; + const data = { + body: (async function* () { + yield { chunk: { bytes: encoder.encode(JSON.stringify(messageStart)) } }; + })(), + }; + + respond(span, 'InvokeModelWithResponseStream', { modelId: CLAUDE_MODEL_ID }, data); + await drain(data.body); + + // The total waits for the final output count, which `message_start` does not carry. + const expected = { ...EXPECTED_INPUT_USAGE, [GEN_AI_USAGE_TOTAL_TOKENS]: undefined }; + expect(usageAttributes(span, expected)).toEqual(expected); + }); +}); diff --git a/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts b/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts new file mode 100644 index 000000000000..16687da1db8a --- /dev/null +++ b/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts @@ -0,0 +1,46 @@ +import { functionToStringIntegration, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { afterAll, describe, expect, it } from 'vitest'; +import type { MatchedHandler } from '../../../src/integrations/hono/honoTypes'; +import { wrapRouteHandlers } from '../../../src/integrations/hono/matchResult'; +import { wrapMiddlewareWithSpan } from '../../../src/integrations/hono/wrapMiddlewareSpan'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +describe('sentryOriginalTraps', () => { + afterAll(() => { + getCurrentScope().setClient(undefined); + }); + + it('lets functionToStringIntegration unwrap a wrapped Hono middleware', async () => { + const client = new TestClient(getDefaultTestClientOptions()); + setCurrentClient(client); + client.addIntegration(functionToStringIntegration()); + + const middleware = async (_c: unknown, next: () => Promise): Promise => { + await next(); + }; + const wrapped = wrapMiddlewareWithSpan(middleware); + + expect(wrapped).not.toBe(middleware); + expect(wrapped.toString()).toBe(middleware.toString()); + }); + + it('lets functionToStringIntegration unwrap a wrapped Hono route handler', async () => { + const client = new TestClient(getDefaultTestClientOptions()); + setCurrentClient(client); + client.addIntegration(functionToStringIntegration()); + + const handler = async (_c: unknown, next: () => Promise): Promise => { + await next(); + }; + const catchAll = (): string => 'fallback'; + const handlers: MatchedHandler[] = [ + [[handler, { method: 'GET', path: '/posts/:id', handler }], {}], + [[catchAll, { method: 'GET', path: '/posts/*', handler: catchAll }], {}], + ]; + wrapRouteHandlers(handlers, [false, false]); + const [[[wrapped]]] = handlers; + + expect(wrapped).not.toBe(handler); + expect(wrapped.toString()).toBe(handler.toString()); + }); +}); diff --git a/packages/server-utils/test/orchestrion/detect.test.ts b/packages/server-utils/test/orchestrion/detect.test.ts index 97edd5223142..041971d83ba0 100644 --- a/packages/server-utils/test/orchestrion/detect.test.ts +++ b/packages/server-utils/test/orchestrion/detect.test.ts @@ -1,6 +1,6 @@ import { GLOBAL_OBJ } from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { getOrchestrionInjectedModules } from '../../src/orchestrion/detect'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { detectOrchestrionSetup, getOrchestrionInjectedModules } from '../../src/orchestrion/detect'; describe('getOrchestrionInjectedModules', () => { beforeEach(() => { @@ -25,3 +25,65 @@ describe('getOrchestrionInjectedModules', () => { expect(getOrchestrionInjectedModules()).toEqual(['pg']); }); }); + +describe('detectOrchestrionSetup', () => { + beforeEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + }); + + afterEach(() => { + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('always warns on Bun when no injection is detected', () => { + vi.stubGlobal('Bun', {}); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('@sentry/bun/plugin')); + }); + + it('warns on Bun only once when called again, as on a repeated `init()`', () => { + vi.stubGlobal('Bun', {}); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + detectOrchestrionSetup(); + + expect(warn).toHaveBeenCalledTimes(1); + }); + + it('does not warn on Bun when no channel integration is installed', () => { + vi.stubGlobal('Bun', {}); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).not.toHaveBeenCalled(); + }); + + it('does not warn on Bun when the bundler plugin ran', () => { + vi.stubGlobal('Bun', {}); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { bundler: new Set(), channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).not.toHaveBeenCalled(); + }); + + it('does not warn outside Bun when debug is off', () => { + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { channelIntegrations: true }; + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + detectOrchestrionSetup(); + + expect(warn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/server-utils/test/orchestrion/instrumentation.test.ts b/packages/server-utils/test/orchestrion/instrumentation.test.ts index 07c61fd5bb7a..0322105fd948 100644 --- a/packages/server-utils/test/orchestrion/instrumentation.test.ts +++ b/packages/server-utils/test/orchestrion/instrumentation.test.ts @@ -46,6 +46,12 @@ describe('invokeOrchestrionInstrumentation', () => { delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; }); + it('records on the marker that a channel integration is installed', () => { + invokeOrchestrionInstrumentation(makeClient() as never, ['mysql'], vi.fn(), []); + + expect(GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.channelIntegrations).toBe(true); + }); + it('subscribes immediately when a module is already injected', () => { GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['mysql'] }; const client = makeClient(); diff --git a/packages/server-utils/test/utils/runtime.test.ts b/packages/server-utils/test/utils/runtime.test.ts new file mode 100644 index 000000000000..e70c0cccedc9 --- /dev/null +++ b/packages/server-utils/test/utils/runtime.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { isBun, isCloudflareWorkers, isDeno } from '../../src/utils/runtime'; + +describe('runtime detection', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('detects none of the runtimes in Node', () => { + expect(isBun()).toBe(false); + expect(isDeno()).toBe(false); + expect(isCloudflareWorkers()).toBe(false); + }); + + it('detects Bun', () => { + vi.stubGlobal('Bun', {}); + expect(isBun()).toBe(true); + }); + + it('detects Deno', () => { + vi.stubGlobal('Deno', {}); + expect(isDeno()).toBe(true); + }); + + it('detects Cloudflare Workers by its exact user agent', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + expect(isCloudflareWorkers()).toBe(true); + + vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0)' }); + expect(isCloudflareWorkers()).toBe(false); + }); +}); diff --git a/packages/solid/package.json b/packages/solid/package.json index 97945931f33d..9b1ae0a11677 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -56,7 +56,7 @@ "dependencies": { "@sentry/browser": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", @@ -80,7 +80,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.8", "solid-js": "^1.9.18", - "vite": "^8.3.5", + "vite": "^8.3.6", "vite-plugin-solid": "^2.11.6" }, "scripts": { @@ -93,9 +93,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.ts && madge --circular src/solidrouter.ts && madge --circular src/tanstackrouter.ts", - "clean": "rimraf build coverage sentry-solid-*.tgz ./*.d.ts ./*.d.ts.map", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.ts src/solidrouter.ts src/tanstackrouter.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-solid-*.tgz ./*.d.ts ./*.d.ts.map", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index b698a4585010..4bae8eb75475 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -87,7 +87,7 @@ "@sentry/server-utils": "11.11.0", "@sentry/solid": "11.11.0", "@sentry/bundler-plugins": "11.11.0", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.32.0", "h3": "^1.15.10" }, "devDependencies": { @@ -100,10 +100,9 @@ "@testing-library/user-event": "^14.6.8", "@types/node": "^20.19.0", "nitropack": "^2.13.4", - "node-mock-http": "^1.0.4", "solid-js": "^1.9.18", "vinxi": "^0.5.11", - "vite": "^8.3.5", + "vite": "^8.3.6", "vite-plugin-solid": "^2.11.6" }, "scripts": { @@ -116,9 +115,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts", - "clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts src/solidrouter.client.ts src/solidrouter.server.ts src/solidrouter.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/solidstart/src/common/tunnel.ts b/packages/solidstart/src/common/tunnel.ts index 9623abbc338e..16f846a83dd0 100644 --- a/packages/solidstart/src/common/tunnel.ts +++ b/packages/solidstart/src/common/tunnel.ts @@ -1,5 +1,5 @@ import type { Options } from '@sentry/core'; -import { _INTERNAL_applyTunnelDefaults as applyTunnelDefaults } from '@sentry/core'; +import { _INTERNAL_applyBrowserTunnelDefaults as applyBrowserTunnelDefaults } from '@sentry/core'; /** Set on the server by `withSentry` to the app's `server.baseURL`. */ export const TUNNEL_BASE_PATH_GLOBAL = '__sentry_solidstart_base_path'; @@ -13,5 +13,5 @@ declare const __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__: string | undefined; export function applyTunnelBasePath(options: Pick): void { const basePath = typeof __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__ !== 'undefined' ? __SENTRY_SOLIDSTART_TUNNEL_BASE_PATH__ : ''; - options.tunnel = applyTunnelDefaults(options.tunnel, { basePath }); + options.tunnel = applyBrowserTunnelDefaults(options.tunnel, { basePath }); } diff --git a/packages/solidstart/src/nitro/tunnelPlugin.ts b/packages/solidstart/src/nitro/tunnelPlugin.ts index c6882b74082d..857dfa8f66af 100644 --- a/packages/solidstart/src/nitro/tunnelPlugin.ts +++ b/packages/solidstart/src/nitro/tunnelPlugin.ts @@ -1,37 +1,15 @@ import { GLOBAL_OBJ } from '@sentry/core'; -import { - _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, -} from '@sentry/core/server'; -import { eventHandler, toWebRequest } from 'h3'; +import { _INTERNAL_setTunnelDefaults as setTunnelDefaults } from '@sentry/core/server'; import type { NitroAppPlugin } from 'nitropack'; import { TUNNEL_BASE_PATH_GLOBAL } from '../common/tunnel'; /** - * Answers managed tunnel requests on SolidStart v1 (Nitro v2), from the first layer of the h3 app. That runs before - * route rules, the app's middleware and routing. `withSentry` registers it. + * Serves the managed tunnel under the app's base path on SolidStart v1 (Nitro v2). The runtime answers the requests. + * `withSentry` registers it. */ -export default (nitroApp => { - // The first layer sees the path before Nitro strips `server.baseURL`, so the tunnel is served under it. The server - // bundle's banner sets the global before this plugin runs. - setManagedTunnelDefaults({ +export default (() => { + // The server bundle's banner sets the global before this plugin runs. + setTunnelDefaults({ basePath: (GLOBAL_OBJ as { [TUNNEL_BASE_PATH_GLOBAL]?: string })[TUNNEL_BASE_PATH_GLOBAL], }); - - // Not the `request` hook: h3 still runs the first layer after a hook answered, so route rules would act on the - // sent response. - nitroApp.h3App.stack.unshift({ - route: '', - handler: eventHandler(event => { - // Checked on the method and path first, so no other request's body gets wrapped in a web `Request`. - if (!isServedTunnelRequest({ method: event.method, url: event.path })) { - return undefined; - } - - // `toWebRequest` also reads the body of the mock requests serverless presets pass through `localFetch`, which - // aren't Node streams. A returned `Response` is sent and ends the request, `undefined` hands it on. - return handleServedTunnelRequest(toWebRequest(event)); - }), - }); }) satisfies NitroAppPlugin; diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts index c4c056319b55..d4074ce9cea5 100644 --- a/packages/solidstart/test/config/withSentry.test.ts +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -129,6 +129,7 @@ describe('withSentry()', () => { 'sentry-solidstart-tunnel-base-path', 'sentry-solidstart-build-instrumentation-file', 'sentry-vite-plugin', + 'sentry-vite-upload-staging', 'sentry-solidstart-update-source-map-setting', ]); }); @@ -153,6 +154,7 @@ describe('withSentry()', () => { 'sentry-solidstart-tunnel-base-path', 'sentry-solidstart-build-instrumentation-file', 'sentry-vite-plugin', + 'sentry-vite-upload-staging', 'sentry-solidstart-update-source-map-setting', 'my-test-plugin', ]); @@ -181,6 +183,7 @@ describe('withSentry()', () => { 'sentry-solidstart-tunnel-base-path', 'sentry-solidstart-build-instrumentation-file', 'sentry-vite-plugin', + 'sentry-vite-upload-staging', 'sentry-solidstart-update-source-map-setting', 'my-test-plugin', ]); diff --git a/packages/solidstart/test/nitro/tunnelPlugin.test.ts b/packages/solidstart/test/nitro/tunnelPlugin.test.ts deleted file mode 100644 index 0f03cef43745..000000000000 --- a/packages/solidstart/test/nitro/tunnelPlugin.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Client } from '@sentry/core'; -import { createEnvelope, getCurrentScope, getTunnelPath, makeDsn, serializeEnvelope } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; -import { createApp, eventHandler, setHeaders, toNodeListener } from 'h3'; -import { fetchNodeRequestHandler } from 'node-mock-http'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import tunnelPlugin from '../../src/nitro/tunnelPlugin'; - -const DSN = 'https://public@o1.ingest.sentry.io/1337'; - -type RequestHook = (event: unknown) => Promise; - -/** - * Calls an h3 app wired like Nitro v2's: the `request` hook runs in `onRequest`, and the route rules layer, here - * with a `headers` rule, comes before the app. Requests come in the way serverless presets pass them through - * `localFetch`, with node-mock-http's request, which isn't a Node stream. - */ -function fetchApp(url: string, init: RequestInit): Promise { - const requestHooks: RequestHook[] = []; - const app = createApp({ - onRequest: async event => { - for (const hook of requestHooks) { - await hook(event); - } - }, - onError: error => onError(error), - }); - app.use(eventHandler(event => setHeaders(event, { 'x-route-rule': '1' }))); - app.use(eventHandler(() => 'app')); - (tunnelPlugin as (nitroApp: unknown) => void)({ - h3App: app, - hooks: { hook: (name: string, fn: RequestHook) => name === 'request' && requestHooks.push(fn) }, - }); - - return fetchNodeRequestHandler(toNodeListener(app), `http://localhost${url}`, init); -} - -const onError = vi.fn(); - -describe('SolidStart v1 tunnel plugin', () => { - afterEach(() => { - getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); - vi.unstubAllGlobals(); - vi.clearAllMocks(); - }); - - it('answers tunnel requests before route rules and the app with `serveTunnelIntegration`', async () => { - const options = { dsn: DSN }; - const client = { getDsn: () => makeDsn(DSN), getOptions: () => options } as unknown as Client; - getCurrentScope().setClient(client); - serveTunnelIntegration().setup!(client); - const sentryFetch = vi.fn((_url: string, _init: RequestInit) => Promise.resolve(new Response('forwarded'))); - vi.stubGlobal('fetch', sentryFetch); - const envelope = serializeEnvelope(createEnvelope({ dsn: DSN }, [])) as string; - - const response = await fetchApp(getTunnelPath(DSN)!, { - method: 'POST', - body: envelope, - headers: { 'content-length': String(envelope.length) }, - }); - - expect(await response.text()).toBe('forwarded'); - expect(await new Response(sentryFetch.mock.lastCall![1].body).text()).toBe(envelope); - // A route rule that ran after the answer would fail to set headers on the sent response. - expect(response.headers.get('x-route-rule')).toBeNull(); - expect(onError).not.toHaveBeenCalled(); - }); - - it('serves no tunnel without `serveTunnelIntegration`', async () => { - const options = { dsn: DSN }; - getCurrentScope().setClient({ getDsn: () => makeDsn(DSN), getOptions: () => options } as unknown as Client); - const envelope = serializeEnvelope(createEnvelope({ dsn: DSN }, [])) as string; - - const response = await fetchApp(getTunnelPath(DSN)!, { - method: 'POST', - body: envelope, - headers: { 'content-length': String(envelope.length) }, - }); - - expect(await response.text()).toBe('app'); - }); - - it('leaves every other request to the app', async () => { - const response = await fetchApp('/api/users', { method: 'POST', body: 'x' }); - - expect(await response.text()).toBe('app'); - expect(response.headers.get('x-route-rule')).toBe('1'); - }); -}); diff --git a/packages/solidstart/test/vite/sentrySolidStart.test.ts b/packages/solidstart/test/vite/sentrySolidStart.test.ts index a2aadf600b2c..cfd7bb1adabb 100644 --- a/packages/solidstart/test/vite/sentrySolidStart.test.ts +++ b/packages/solidstart/test/vite/sentrySolidStart.test.ts @@ -54,6 +54,7 @@ describe('sentrySolidStart()', () => { 'code-transformer', 'sentry-solidstart-files-to-delete-after-upload', 'sentry-vite-plugin', + 'sentry-vite-upload-staging', 'sentry-solidstart-update-source-map-setting', ]); }); diff --git a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts index db2382efdc45..3273693769cf 100644 --- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts +++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts @@ -29,6 +29,7 @@ describe('sentrySolidStartVite()', () => { 'sentry-solidstart-tunnel-base-path', 'sentry-solidstart-build-instrumentation-file', 'sentry-vite-plugin', + 'sentry-vite-upload-staging', 'sentry-solidstart-update-source-map-setting', ]); }); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c17280b6770a..8fe811e474fa 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "magic-string": "~0.30.0" }, @@ -45,7 +45,7 @@ "@sveltejs/vite-plugin-svelte": "1.4.0", "@testing-library/svelte": "^3.2.1", "svelte": "3.59.2", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "scripts": { "build": "run-p build:transpile build:types", @@ -55,9 +55,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-svelte-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-svelte-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 5e1bc856e5fe..be05695b7451 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -73,7 +73,7 @@ "@opentelemetry/api": "^1.9.1", "@sentry/cloudflare": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", @@ -91,7 +91,7 @@ "@sveltejs/vite-plugin-svelte": "^3.0.0", "@types/node": "^20.19.0", "svelte": "^4.2.8", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "scripts": { "build": "run-p build:transpile build:types", @@ -101,9 +101,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-sveltekit-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-sveltekit-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/sveltekit/rollup.npm.config.mjs b/packages/sveltekit/rollup.npm.config.mjs index b6da582e41fc..a2ee50611cd8 100644 --- a/packages/sveltekit/rollup.npm.config.mjs +++ b/packages/sveltekit/rollup.npm.config.mjs @@ -20,7 +20,13 @@ export default makeNPMConfigVariants( packageSpecificConfig: { // Keep the variant subpath external so the transpiled output preserves the import for the // consumer to resolve (via `exports` or the `sentrySvelteKit()` plugin). - external: ['$app/state', '$app/stores', '@sentry/sveltekit/browser-tracing-variant', 'cloudflare:workers'], + external: [ + 'svelte', + '$app/state', + '$app/stores', + '@sentry/sveltekit/browser-tracing-variant', + 'cloudflare:workers', + ], output: { dynamicImportInCjs: true, }, diff --git a/packages/sveltekit/src/client/navigationState.svelte.ts b/packages/sveltekit/src/client/navigationState.svelte.ts index 0273dee73bf7..9df675259569 100644 --- a/packages/sveltekit/src/client/navigationState.svelte.ts +++ b/packages/sveltekit/src/client/navigationState.svelte.ts @@ -1,5 +1,6 @@ import type { Navigation } from '@sveltejs/kit'; import { navigating, page } from '$app/state'; +import { untrack } from 'svelte'; declare const $effect: { (callback: () => void): void; @@ -31,9 +32,11 @@ export function onNavigationChange(callback: (navigation: Navigation | null) => * Reads the current navigation synchronously. `$effect`s are always batched to a microtask, which is * too late to catch SvelteKit's synchronous data-request `fetch` at navigation start; a synchronous * rune read lets us start the navigation span before that request so it propagates the right trace. + * The read is untracked: the `fetch` wrapper calls this from user code, often inside a `$effect`, + * and a tracked read would make that effect re-run on every navigation. */ export function getCurrentNavigation(): Navigation | null { - return _snapshot(); + return untrack(_snapshot); } function _snapshot(): Navigation | null { diff --git a/packages/sveltekit/src/client/svelte5.d.ts b/packages/sveltekit/src/client/svelte5.d.ts new file mode 100644 index 000000000000..40d450d25dd8 --- /dev/null +++ b/packages/sveltekit/src/client/svelte5.d.ts @@ -0,0 +1,7 @@ +// The package is type-checked against Svelte 4, but the Kit 3 variant only ever runs on Svelte 5. +// Declares the Svelte 5 export it uses; build-time only and never emitted. +export {}; + +declare module 'svelte' { + export function untrack(fn: () => T): T; +} diff --git a/packages/sveltekit/src/common/tunnel.ts b/packages/sveltekit/src/common/tunnel.ts index f94fb9878001..bab63d505b77 100644 --- a/packages/sveltekit/src/common/tunnel.ts +++ b/packages/sveltekit/src/common/tunnel.ts @@ -1,5 +1,5 @@ import type { Options } from '@sentry/core'; -import { _INTERNAL_applyTunnelDefaults as applyTunnelDefaults, GLOBAL_OBJ } from '@sentry/core'; +import { _INTERNAL_applyBrowserTunnelDefaults as applyBrowserTunnelDefaults, GLOBAL_OBJ } from '@sentry/core'; /** Set by `sentrySvelteKit()` to the app's `paths.base`. */ export const TUNNEL_BASE_PATH_GLOBAL = '__sentry_sveltekit_base_path'; @@ -16,5 +16,5 @@ export function getTunnelBasePath(): string | undefined { * A `basePath` the user set wins. */ export function applyTunnelBasePath(options: Pick): void { - options.tunnel = applyTunnelDefaults(options.tunnel, { basePath: getTunnelBasePath() }); + options.tunnel = applyBrowserTunnelDefaults(options.tunnel, { basePath: getTunnelBasePath() }); } diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 5ee8c6faca30..31e2aa472371 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -56,6 +56,7 @@ export declare const withStreamedSpan: typeof clientSdk.withStreamedSpan; // Different implementation in server and worker export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration; +export declare const serveTunnelIntegration: typeof serverSdk.serveTunnelIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index f899ad926ed3..38291b226679 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -22,7 +22,7 @@ import { import { flushIfServerless, _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { getTunnelBasePath } from '../common/tunnel'; import type { Handle, ResolveOptions } from './handleTypes'; @@ -292,7 +292,7 @@ async function instrumentHandle( export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // The hooks file creates this after the `sentrySvelteKit()` base path module ran, however the SDK was started // (`--import`, a custom server), and later per-request clients (Cloudflare) start with it too. - setManagedTunnelDefaults({ basePath: getTunnelBasePath() }); + setTunnelDefaults({ basePath: getTunnelBasePath() }); const { handleUnknownRoutes, ...rest } = handlerOptions ?? {}; const options = { diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 7cfa82ed6f6d..4394e27ad4bf 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -77,6 +77,7 @@ export async function makeCustomSentryVitePlugins( sourcemaps: { ...options?.sourcemaps, filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise, + deleteSourcemapsAfterUpload: false, }, }; @@ -364,7 +365,7 @@ function detectSentryRelease(): string { let releaseFallback: string; try { releaseFallback = child_process - .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }) + .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }) .toString() .trim(); } catch { diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts index 4a4fd545f738..9e55877cfdad 100644 --- a/packages/sveltekit/test/server-common/handle.test.ts +++ b/packages/sveltekit/test/server-common/handle.test.ts @@ -3,7 +3,7 @@ import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attr import { getCapturedScopesOnSpan, getRootSpan, getSpanDescendants, spanIsSampled, spanToJSON } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { NodeClient, serveTunnelIntegration, setCurrentClient } from '@sentry/node'; -import { _INTERNAL_resetManagedTunnelDefaults } from '@sentry/core/server'; +import { _INTERNAL_resetTunnelDefaults } from '@sentry/core/server'; import { TUNNEL_BASE_PATH_GLOBAL } from '../../src/common/tunnel'; import type { Handle } from '../../src/server-common/handleTypes'; import { redirect } from '@sveltejs/kit'; @@ -168,7 +168,7 @@ describe('sentryHandle with the managed tunnel', () => { afterEach(() => { vi.unstubAllGlobals(); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); }); it('answers tunnel requests without resolving the route', async () => { diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 7cae5e1097c4..cfb578882e35 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -70,8 +70,8 @@ describe('sentrySvelteKit()', () => { // 1 kit config resolver + 1 browser-tracing variant resolver + 1 OpenTelemetry API resolver // + 1 tunnel base path plugin + 1 auto instrument plugin // + 1 orchestrion plugin + 1 build flag plugin + 1 global values injection plugin + 1 modified main plugin - // + 3 custom plugins - expect(plugins).toHaveLength(12); + // + 1 debug ID plugin + 3 custom plugins + expect(plugins).toHaveLength(13); }); it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => { @@ -96,6 +96,7 @@ describe('sentrySvelteKit()', () => { 'sentry-sveltekit-global-values-injection-plugin', // modified main plugin (writeBundle deferred to closeBundle): 'sentry-vite-plugin', + 'sentry-vite-upload-staging', 'sentry-sveltekit-update-source-map-setting-plugin', 'sentry-sveltekit-files-to-delete-after-upload-setting-plugin', // custom source maps plugin (sorcery flattening + deferred upload): @@ -124,7 +125,7 @@ describe('sentrySvelteKit()', () => { it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => { const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false }); const pluginNames = plugins.map(plugin => plugin.name); - expect(plugins).toHaveLength(11); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + tunnel base path + orchestrion + build flag + global values injection + 1 modified main plugin + 3 custom plugins + expect(plugins).toHaveLength(12); // kit config resolver + browser-tracing variant resolver + OpenTelemetry API resolver + tunnel base path + orchestrion + build flag + global values injection + 1 modified main plugin + 1 debug ID plugin + 3 custom plugins expect(pluginNames).not.toContain('sentry-auto-instrumentation'); }); diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index d4f2d3c06db8..a2d1684118a5 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -1,4 +1,5 @@ import { sentryVitePlugin } from '@sentry/bundler-plugins/vite'; +import * as childProcess from 'child_process'; import type { Plugin } from 'vite'; import * as vite from 'vite'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -10,6 +11,8 @@ const mockedMainPlugin = { writeBundle: vi.fn(), }; +vi.mock('child_process', { spy: true }); + vi.mock('@sentry/bundler-plugins/vite', async () => { const original = (await vi.importActual('@sentry/bundler-plugins/vite')) as any; @@ -54,6 +57,22 @@ async function getSentryViteSubPlugin(name: string): Promise return plugins.find(plugin => plugin.name === name); } +describe('release detection', () => { + it('hides the console when detecting the git revision', async () => { + vi.resetModules(); + const execSpy = vi + .mocked(childProcess.execSync) + .mockReturnValueOnce(Buffer.from('4316c2634a5f3de43cbd63780117d9221a23e3cd\n')); + + await import('../../src/vite/sourceMaps'); + + expect(execSpy).toHaveBeenCalledWith('git rev-parse HEAD', { + stdio: ['ignore', 'pipe', 'ignore'], + windowsHide: true, + }); + }); +}); + describe('makeCustomSentryVitePlugins()', () => { beforeEach(() => { // @ts-expect-error - this function exists! @@ -385,6 +404,7 @@ describe('deleteFilesAfterUpload', () => { }, sourcemaps: { filesToDeleteAfterUpload: expect.any(Promise), + deleteSourcemapsAfterUpload: false, }, }); @@ -463,6 +483,7 @@ describe('deleteFilesAfterUpload', () => { }, sourcemaps: { filesToDeleteAfterUpload: expect.any(Promise), + deleteSourcemapsAfterUpload: false, }, }); diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index f056cfdf6c50..cca63a18f138 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -53,7 +53,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser-utils": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", @@ -64,7 +64,7 @@ "devDependencies": { "@sentry-internal/rollup-utils": "11.11.0", "@types/node": "^20.19.0", - "vite": "^8.3.5" + "vite": "^8.3.6" }, "scripts": { "build": "run-p build:transpile build:types", @@ -74,9 +74,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", - "clean": "rimraf build coverage sentry-tanstackstart-react-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-tanstackstart-react-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/tanstackstart-react/src/client/index.ts b/packages/tanstackstart-react/src/client/index.ts index b157b4f31173..ad7946b233b4 100644 --- a/packages/tanstackstart-react/src/client/index.ts +++ b/packages/tanstackstart-react/src/client/index.ts @@ -64,10 +64,8 @@ export function createSentryTunnelRoute(_options: CreateSentryTunnelRouteOptions export function registerSentryServerTunnelRoute(_path: string): void {} /** - * No-op stub for client-side builds. The server entry the Vite plugin wraps only runs on the server. + * No-op stub for client-side builds. The server entry that calls it only runs on the server. * * @internal */ -export function _INTERNAL_wrapServerEntryWithSentryTunnel(entry: T, _options?: unknown): T { - return entry; -} +export function _INTERNAL_setTunnelRoute(_tunnelRoute?: unknown, _basePath?: string): void {} diff --git a/packages/tanstackstart-react/src/client/tunnelRoute.ts b/packages/tanstackstart-react/src/client/tunnelRoute.ts index f4a676b63d70..e612d1b6219b 100644 --- a/packages/tanstackstart-react/src/client/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/client/tunnelRoute.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_applyTunnelDefaults as applyTunnelDefaults, consoleSandbox } from '@sentry/core'; +import { _INTERNAL_applyBrowserTunnelDefaults as applyBrowserTunnelDefaults, consoleSandbox } from '@sentry/core'; import type { BrowserOptions as ReactBrowserOptions } from '@sentry/react'; declare const __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: string | true | undefined; @@ -16,7 +16,7 @@ export function applyTunnelRouteOption(options: ReactBrowserOptions): void { const basePath = typeof __SENTRY_TANSTACKSTART_TUNNEL_BASE_PATH__ !== 'undefined' ? __SENTRY_TANSTACKSTART_TUNNEL_BASE_PATH__ : ''; - options.tunnel = applyTunnelDefaults(options.tunnel, { basePath }); + options.tunnel = applyBrowserTunnelDefaults(options.tunnel, { basePath }); } function applyManagedTunnelRoute(options: ReactBrowserOptions): void { diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 9b81217a239a..b06807027a1c 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -49,4 +49,4 @@ export declare const sentryGlobalRequestMiddleware: typeof serverSdk.sentryGloba export declare const sentryGlobalFunctionMiddleware: typeof serverSdk.sentryGlobalFunctionMiddleware; export declare const createSentryTunnelRoute: typeof serverSdk.createSentryTunnelRoute; export declare const registerSentryServerTunnelRoute: typeof serverSdk.registerSentryServerTunnelRoute; -export declare const _INTERNAL_wrapServerEntryWithSentryTunnel: typeof serverSdk._INTERNAL_wrapServerEntryWithSentryTunnel; +export declare const _INTERNAL_setTunnelRoute: typeof serverSdk._INTERNAL_setTunnelRoute; diff --git a/packages/tanstackstart-react/src/server/index.ts b/packages/tanstackstart-react/src/server/index.ts index cef85cd391c4..b537fd9cfd01 100644 --- a/packages/tanstackstart-react/src/server/index.ts +++ b/packages/tanstackstart-react/src/server/index.ts @@ -12,7 +12,7 @@ export { sentryGlobalRequestMiddleware, sentryGlobalFunctionMiddleware } from '. export { createSentryTunnelRoute, registerSentryServerTunnelRoute, - wrapServerEntryWithSentryTunnel as _INTERNAL_wrapServerEntryWithSentryTunnel, + setTunnelRoute as _INTERNAL_setTunnelRoute, } from './tunnelRoute'; /** diff --git a/packages/tanstackstart-react/src/server/sdk.ts b/packages/tanstackstart-react/src/server/sdk.ts index 5f0d5a5a29df..987f1105ebc5 100644 --- a/packages/tanstackstart-react/src/server/sdk.ts +++ b/packages/tanstackstart-react/src/server/sdk.ts @@ -1,6 +1,10 @@ import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node'; +import { + _INTERNAL_installTunnelHooks as installTunnelHooks, + getDefaultIntegrations as getDefaultNodeIntegrations, + init as initNodeSdk, +} from '@sentry/node'; /** * Initializes the server side of the TanStack Start React SDK @@ -21,5 +25,10 @@ export function init(options: NodeOptions): NodeClient | undefined { /\/@vite\//, ]; - return initNodeSdk(sentryOptions); + const client = initNodeSdk(sentryOptions); + // `tunnelRoute` enables the tunnel when the server entry loads, after this preload, so the servers must be ready to + // answer it already. + installTunnelHooks(); + + return client; } diff --git a/packages/tanstackstart-react/src/server/tunnelRoute.ts b/packages/tanstackstart-react/src/server/tunnelRoute.ts index 1d4d09b80553..b1f87d3751fd 100644 --- a/packages/tanstackstart-react/src/server/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/server/tunnelRoute.ts @@ -3,11 +3,8 @@ import type { ServerTunnelOptions } from '@sentry/core'; import { dsnToString, escapeStringForRegex, getClient, handleTunnelRequest } from '@sentry/core'; import { _INTERNAL_forwardTunnelRequest as forwardTunnelRequest, - _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; -import type { ServerEntry } from './wrapFetchWithSentry'; const registeredTunnelRoutePaths = new Set(); @@ -99,46 +96,15 @@ export function createSentryTunnelRoute(options: CreateSentryTunnelRouteOptions) } /** - * Answers managed tunnel requests before the server entry runs. The `sentryTanstackStart` Vite plugin applies this - * to the app's server entry automatically, with its `tunnelRoute` and the base path Start routes on. + * Enables the managed tunnel when the server entry loads, which the runtime then answers. The `sentryTanstackStart` + * Vite plugin calls this from the app's server entry, with its `tunnelRoute` and the base path Start routes on. * * `tunnelRoute` enables the managed tunnel on every server client the same way `serveTunnelIntegration` does, unless * that integration is set up too, and the tunnel is then served from the client's config like in every other SDK. * * @internal Only the code the Vite plugin generates calls this, so its parameters can change at any time. */ -export function wrapServerEntryWithSentryTunnel( - entry: T, - tunnelRoute?: ServerTunnelOptions, - basePath?: string, -): T { - // Runs when the server entry loads, before any request. Requests still carry Start's base path here, so the tunnel - // is served under it. - setManagedTunnelDefaults({ tunnel: tunnelRoute, forward: forwardTunnelRequest, basePath }); - - // A class entry, like Cloudflare's `WorkerEntrypoint`, can't be spread into an object. The tunnel is then served - // by the wrapped default server entry it calls. - if (typeof entry === 'function') { - return entry; - } - - // Keeps the prototype, so a class instance entry keeps its other handlers, like `scheduled` or `queue`. `fetch` is - // defined rather than assigned, which a read-only `fetch` on a frozen prototype would reject. - return Object.create(Object.getPrototypeOf(entry), { - ...Object.getOwnPropertyDescriptors(entry), - fetch: { - // Cloudflare Workers pass `env` and `ctx` after the request, and `withSentry` needs `ctx` to flush. - value: (request: Request, ...rest: unknown[]) => { - // Start would render the app for any other method on a tunnel path, running its root loaders. - if (request.method !== 'POST' && isServedTunnelRequest({ method: 'POST', url: request.url })) { - return new Response(null, { status: 405, headers: { Allow: 'POST' } }); - } - - return handleServedTunnelRequest(request) ?? entry.fetch(request, ...rest); - }, - writable: true, - configurable: true, - enumerable: true, - }, - }) as T; +export function setTunnelRoute(tunnelRoute?: ServerTunnelOptions, basePath?: string): void { + // Requests still carry Start's base path, so the tunnel is served under it. + setTunnelDefaults({ tunnel: tunnelRoute, forward: forwardTunnelRequest, basePath }); } diff --git a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts index 6d56f81a63e7..a93360039525 100644 --- a/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts +++ b/packages/tanstackstart-react/src/vite/sentryTanstackStart.ts @@ -8,6 +8,8 @@ import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourc import type { TunnelRouteOptions } from './tunnelRoute'; import { makeServerEntryTunnelPlugin, makeTunnelBasePathPlugin, makeTunnelRoutePlugin } from './tunnelRoute'; +let showedDeprecatedTunnelRouteWarning = false; + /** * Build-time options for the Sentry TanStack Start SDK. */ @@ -47,6 +49,9 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase { * * If you also pass `tunnel` to `Sentry.init()`, that explicit runtime option wins and a warning is emitted because * the managed tunnel route is being bypassed. + * + * @deprecated Set `tunnel: true` in the browser `Sentry.init` and add `serveTunnelIntegration()` to the server + * `Sentry.init` instead. */ tunnelRoute?: TunnelRouteOptions; } @@ -76,15 +81,25 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase { * @returns An array of Vite plugins */ export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): Plugin[] { + // oxlint-disable-next-line typescript/no-deprecated -- reads the deprecated option to keep it working + const { tunnelRoute } = options; + if (tunnelRoute !== undefined && !showedDeprecatedTunnelRouteWarning) { + showedDeprecatedTunnelRouteWarning = true; + // eslint-disable-next-line no-console + console.warn( + '[@sentry/tanstackstart-react] The `tunnelRoute` option is deprecated. Set `tunnel: true` in the browser `Sentry.init` and add `serveTunnelIntegration()` to the server `Sentry.init` instead.', + ); + } + const tunnelBasePath = makeTunnelBasePathPlugin(); const plugins: Plugin[] = [ makeRoutePatternPlugin(), tunnelBasePath.plugin, - makeServerEntryTunnelPlugin(options.tunnelRoute, tunnelBasePath.getBasePath), + makeServerEntryTunnelPlugin(tunnelRoute, tunnelBasePath.getBasePath), ]; - if (options.tunnelRoute) { - plugins.push(makeTunnelRoutePlugin(options.tunnelRoute, options.debug)); + if (tunnelRoute) { + plugins.push(makeTunnelRoutePlugin(tunnelRoute, options.debug)); } // only add build-time plugins in production builds diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts index cb69c605b918..cc280e24b514 100644 --- a/packages/tanstackstart-react/src/vite/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/vite/tunnelRoute.ts @@ -2,7 +2,7 @@ import type { ServerTunnelOptions } from '@sentry/core'; import { _INTERNAL_normalizeTunnelBasePath as normalizeTunnelBasePath } from '@sentry/core'; import { _INTERNAL_forwardTunnelRequest as forwardTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import type { Plugin } from 'vite'; @@ -67,7 +67,7 @@ function hasRouteConflict(source: string, tunnelPath: string): boolean { } /** - * Tells the client SDK which tunnel to use. The server entry wrapper serves it. + * Tells the client SDK which tunnel to use. The runtime serves it. */ export function makeTunnelRoutePlugin(options: TunnelRouteOptions, debug?: boolean): Plugin { const { path } = normalizeTunnelRouteOptions(options); @@ -82,7 +82,7 @@ export function makeTunnelRoutePlugin(options: TunnelRouteOptions, debug?: boole config() { return { define: { __SENTRY_TANSTACKSTART_TUNNEL_ROUTE__: path ? JSON.stringify(path) : 'true' } }; }, - // The server entry wrapper answers POSTs to a fixed path before Start's routing, so an app route on that path + // The runtime answers POSTs to a fixed path before Start's routing, so an app route on that path // would silently lose them. transform(source, id) { if (path && /[\\/]routeTree\.gen\.ts$/.test(id) && hasRouteConflict(source, path)) { @@ -99,7 +99,7 @@ export function makeTunnelRoutePlugin(options: TunnelRouteOptions, debug?: boole const START_BASE_PATH_DEFINE = 'process.env.TSS_ROUTER_BASEPATH'; /** - * Reads the base path TanStack Start routes on and hands it to the client SDK and the server entry wrapper, so the + * Reads the base path TanStack Start routes on and hands it to the client SDK and the server entry, so the * managed tunnel is served under it without the user repeating it. Start resolves it from `router.basepath`, or from * Vite's `base`, and defines it for every environment. This runs after Start's own `config` hook to see that define. */ @@ -144,9 +144,8 @@ const ORIGINAL_SERVER_ENTRY_QUERY = '?sentry-tunnel-original'; const DEFAULT_SERVER_ENTRY = /\/react-start\/dist\/(?:plugin\/)?default-entry\/(?:esm\/)?server\.[jt]s$/; /** - * Wraps the TanStack Start server entry so tunnel requests are answered before Start's own request middleware and - * routing. It does nothing unless the managed tunnel is enabled, either here via `tunnelRoute` or at runtime via - * `Sentry.init({ tunnel: true })`. + * Enables `tunnelRoute` when the TanStack Start server entry loads, under the base path Start routes on. The runtime + * then answers the tunnel requests before Start's own request middleware and routing. */ export function makeServerEntryTunnelPlugin( tunnelRoute: TunnelRouteOptions | undefined, @@ -171,7 +170,7 @@ export function makeServerEntryTunnelPlugin( // `vite dev` takes each request in this process and passes it on to where the server entry runs, so its // `http.server` span comes from the SDK `--import` set up here, which the server entry never reaches. configureServer() { - setManagedTunnelDefaults({ + setTunnelDefaults({ tunnel: normalizedTunnelOptions, forward: forwardTunnelRequest, basePath: getBasePath(), @@ -190,10 +189,10 @@ export function makeServerEntryTunnelPlugin( const resolved = await this.resolve(SDK_PACKAGE, `${root}/package.json`, { skipSelf: true }); const sdk = resolved && !resolved.external ? resolved.id : SDK_PACKAGE; - return `import entry from ${original}; -import { _INTERNAL_wrapServerEntryWithSentryTunnel as wrapServerEntryWithSentryTunnel } from ${JSON.stringify(sdk)}; + return `import { _INTERNAL_setTunnelRoute as setTunnelRoute } from ${JSON.stringify(sdk)}; export * from ${original}; -export default wrapServerEntryWithSentryTunnel(entry, ${tunnelOptions}, ${JSON.stringify(getBasePath())}); +export { default } from ${original}; +setTunnelRoute(${tunnelOptions}, ${JSON.stringify(getBasePath())}); `; }, }; diff --git a/packages/tanstackstart-react/test/server/setTunnelRoute.test.ts b/packages/tanstackstart-react/test/server/setTunnelRoute.test.ts new file mode 100644 index 000000000000..e8575146b42a --- /dev/null +++ b/packages/tanstackstart-react/test/server/setTunnelRoute.test.ts @@ -0,0 +1,38 @@ +import type { Client } from '@sentry/core'; +import { getCurrentScope, getTunnelPath, makeDsn } from '@sentry/core'; +import { _INTERNAL_resetTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; +import { afterEach, describe, expect, it } from 'vitest'; +import { setTunnelRoute } from '../../src/server/tunnelRoute'; + +const DSN = 'https://public@o1.ingest.sentry.io/1337'; + +/** Sets a client, with `serveTunnelIntegration` set up when `managedTunnel` is given. */ +function setClient(managedTunnel: object | undefined): void { + const options = { dsn: DSN, ignoreSpans: [] }; + const client = { getDsn: () => makeDsn(DSN), getOptions: () => options } as unknown as Client; + getCurrentScope().setClient(client); + if (managedTunnel) { + serveTunnelIntegration(managedTunnel).setup!(client); + } +} + +describe('setTunnelRoute', () => { + afterEach(() => { + getCurrentScope().setClient(undefined); + _INTERNAL_resetTunnelDefaults(); + }); + + it('enables `tunnelRoute` on the client like `serveTunnelIntegration` would, dropping its spans from the start', () => { + setClient(undefined); + const options = getCurrentScope().getClient()!.getOptions(); + + setTunnelRoute({}); + + expect(options._managedTunnel).toEqual({}); + expect( + (options.ignoreSpans![0] as { attributes: { 'url.path': RegExp } }).attributes['url.path'].test( + getTunnelPath(DSN)!, + ), + ).toBe(true); + }); +}); diff --git a/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts b/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts deleted file mode 100644 index b35224c6e277..000000000000 --- a/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { Client } from '@sentry/core'; -import { createEnvelope, getCurrentScope, getTunnelPath, makeDsn, serializeEnvelope } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { wrapServerEntryWithSentryTunnel } from '../../src/server/tunnelRoute'; - -const DSN = 'https://public@o1.ingest.sentry.io/1337'; -const TUNNEL_URL = `http://localhost:3000${getTunnelPath(DSN)}`; - -function makeEnvelopeRequest(url: string): Request { - const body = serializeEnvelope(createEnvelope({ dsn: DSN }, [])) as string; - - return new Request(url, { method: 'POST', body, headers: { 'content-length': String(body.length) } }); -} - -/** Sets a client, with `serveTunnelIntegration` set up when `managedTunnel` is given. */ -function setClient(managedTunnel: object | undefined): void { - const options = { dsn: DSN, ignoreSpans: [] }; - const client = { getDsn: () => makeDsn(DSN), getOptions: () => options } as unknown as Client; - getCurrentScope().setClient(client); - if (managedTunnel) { - serveTunnelIntegration(managedTunnel).setup!(client); - } -} - -describe('wrapServerEntryWithSentryTunnel', () => { - const appFetch = vi.fn(() => new Response('app')); - const sentryFetch = vi.fn(() => Promise.resolve(new Response('forwarded'))); - - beforeEach(() => { - vi.stubGlobal('fetch', sentryFetch); - }); - - afterEach(() => { - getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); - vi.unstubAllGlobals(); - vi.clearAllMocks(); - }); - - it('answers the tunnel path when `serveTunnelIntegration` is set up, without calling the app', async () => { - setClient({}); - - const response = await wrapServerEntryWithSentryTunnel({ fetch: appFetch }).fetch(makeEnvelopeRequest(TUNNEL_URL)); - - expect(await response.text()).toBe('forwarded'); - expect(appFetch).not.toHaveBeenCalled(); - }); - - it('hands every request to the app when the managed tunnel is off', async () => { - setClient(undefined); - - const response = await wrapServerEntryWithSentryTunnel({ fetch: appFetch }).fetch(makeEnvelopeRequest(TUNNEL_URL)); - - expect(await response.text()).toBe('app'); - expect(sentryFetch).not.toHaveBeenCalled(); - }); - - it('serves the tunnel with build-time options even without `serveTunnelIntegration`', async () => { - setClient(undefined); - - const response = await wrapServerEntryWithSentryTunnel({ fetch: appFetch }, {}).fetch( - makeEnvelopeRequest(TUNNEL_URL), - ); - - expect(await response.text()).toBe('forwarded'); - }); - - it('rejects other methods on a fixed `tunnelRoute` path instead of rendering the app', async () => { - setClient(undefined); - const entry: { fetch: (request: Request) => Response } = { fetch: appFetch }; - const wrapped = wrapServerEntryWithSentryTunnel(entry, { path: '/monitor' }); - - const response = await wrapped.fetch(new Request('http://localhost:3000/monitor')); - - expect(response.status).toBe(405); - expect(response.headers.get('allow')).toBe('POST'); - expect(appFetch).not.toHaveBeenCalled(); - expect(await (await wrapped.fetch(new Request('http://localhost:3000/other'))).text()).toBe('app'); - }); - - it('rejects other methods on the DSN-derived tunnel path too', async () => { - setClient({}); - const entry: { fetch: (request: Request) => Response } = { fetch: appFetch }; - - const response = await wrapServerEntryWithSentryTunnel(entry).fetch(new Request(TUNNEL_URL)); - - expect(response.status).toBe(405); - expect(response.headers.get('allow')).toBe('POST'); - expect(appFetch).not.toHaveBeenCalled(); - }); - - it('passes other requests and their options to the app', async () => { - setClient({}); - const request = makeEnvelopeRequest('http://localhost:3000/api/users'); - - await wrapServerEntryWithSentryTunnel({ fetch: appFetch }).fetch(request, { context: 1 }); - - expect(appFetch).toHaveBeenCalledWith(request, { context: 1 }); - }); - - it("passes every argument to the app, like a Cloudflare Worker's `env` and `ctx`", async () => { - setClient({}); - const request = makeEnvelopeRequest('http://localhost:3000/api/users'); - const env = { E2E_TEST_DSN: DSN }; - const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn() }; - - await wrapServerEntryWithSentryTunnel({ fetch: appFetch }).fetch(request, env, ctx); - - expect(appFetch).toHaveBeenCalledWith(request, env, ctx); - }); - - it("keeps a class instance entry's prototype handlers, like a Worker's `scheduled`", async () => { - setClient({}); - class Handler { - public fetch(_request: Request): Response { - return new Response('app'); - } - - public scheduled(): string { - return 'scheduled'; - } - } - - const wrapped = wrapServerEntryWithSentryTunnel(new Handler()); - - expect(wrapped.scheduled()).toBe('scheduled'); - expect(wrapped).toBeInstanceOf(Handler); - expect(await (await wrapped.fetch(makeEnvelopeRequest(TUNNEL_URL))).text()).toBe('forwarded'); - }); - - it('wraps an entry whose prototype is frozen', async () => { - setClient({}); - class Handler { - public fetch(_request: Request): Response { - return new Response('app'); - } - } - Object.freeze(Handler.prototype); - - const wrapped = wrapServerEntryWithSentryTunnel(new Handler()); - - expect(await (await wrapped.fetch(makeEnvelopeRequest(TUNNEL_URL))).text()).toBe('forwarded'); - }); - - it('returns a class entry, like a `WorkerEntrypoint`, unchanged', () => { - class Worker { - public fetch(): Response { - return new Response('app'); - } - } - - expect(wrapServerEntryWithSentryTunnel(Worker as unknown as { fetch: () => Response })).toBe(Worker); - }); - - it('enables `tunnelRoute` on the client like `serveTunnelIntegration` would, dropping its spans from the start', () => { - setClient(undefined); - const options = getCurrentScope().getClient()!.getOptions(); - - wrapServerEntryWithSentryTunnel({ fetch: appFetch }, {}); - - expect(options._managedTunnel).toEqual({}); - expect( - (options.ignoreSpans![0] as { attributes: { 'url.path': RegExp } }).attributes['url.path'].test( - getTunnelPath(DSN)!, - ), - ).toBe(true); - }); - - it('keeps the options of `serveTunnelIntegration` over `tunnelRoute`', async () => { - setClient({ path: '/from-init' }); - const options = getCurrentScope().getClient()!.getOptions(); - - const response = await wrapServerEntryWithSentryTunnel({ fetch: appFetch }, { path: '/from-build' }).fetch( - makeEnvelopeRequest('http://localhost:3000/from-init'), - ); - - expect(options._managedTunnel).toEqual({ path: '/from-init' }); - expect(await response.text()).toBe('forwarded'); - }); - - it('answers the tunnel under the base path Start routes on, and only there', async () => { - setClient({}); - const wrapped = wrapServerEntryWithSentryTunnel({ fetch: appFetch }, undefined, '/app'); - - expect( - await (await wrapped.fetch(makeEnvelopeRequest(`http://localhost:3000/app${getTunnelPath(DSN)}`))).text(), - ).toBe('forwarded'); - expect(await (await wrapped.fetch(makeEnvelopeRequest(TUNNEL_URL))).text()).toBe('app'); - }); - - it('keeps a base path `serveTunnelIntegration` set', async () => { - setClient({ basePath: '/custom' }); - const wrapped = wrapServerEntryWithSentryTunnel({ fetch: appFetch }, undefined, '/app'); - - expect( - await (await wrapped.fetch(makeEnvelopeRequest(`http://localhost:3000/custom${getTunnelPath(DSN)}`))).text(), - ).toBe('forwarded'); - }); -}); diff --git a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts index e757cd9d6173..c20de29b89bc 100644 --- a/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts +++ b/packages/tanstackstart-react/test/vite/sentryTanstackStart.test.ts @@ -303,6 +303,25 @@ describe('sentryTanstackStart()', () => { expect(makeTunnelRoutePlugin).toHaveBeenCalledWith(options.tunnelRoute, undefined); expect(makeServerEntryTunnelPlugin).toHaveBeenCalledWith(options.tunnelRoute, expect.any(Function)); }); + + it('warns once that `tunnelRoute` is deprecated', async () => { + // The warning is shown once per process, so this needs a module that hasn't warned yet. + vi.resetModules(); + const { sentryTanstackStart: freshSentryTanstackStart } = await import('../../src/vite/sentryTanstackStart'); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + freshSentryTanstackStart({ sourcemaps: { disable: true } }); + expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining('`tunnelRoute` option is deprecated')); + + freshSentryTanstackStart({ tunnelRoute: true, sourcemaps: { disable: true } }); + freshSentryTanstackStart({ tunnelRoute: '/monitor', sourcemaps: { disable: true } }); + + expect(warnSpy).toHaveBeenCalledOnce(); + expect(warnSpy).toHaveBeenCalledWith( + '[@sentry/tanstackstart-react] The `tunnelRoute` option is deprecated. Set `tunnel: true` in the browser `Sentry.init` and add `serveTunnelIntegration()` to the server `Sentry.init` instead.', + ); + warnSpy.mockRestore(); + }); }); describe('orchestrion build-time instrumentation', () => { diff --git a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts index 2d895e67f249..110b1ccae40b 100644 --- a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts +++ b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/core'; import { getCurrentScope, getTunnelPath, makeDsn } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; +import { _INTERNAL_resetTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; import type { Plugin, ResolvedConfig, UserConfig } from 'vite'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { @@ -94,17 +94,17 @@ describe('server entry tunnel vite plugin', () => { ) as Promise; } - it('wraps the app server entry and the default entry', async () => { + it('sets the tunnel route from the app server entry and the default entry', async () => { const load = setup(); - expect(await load(USER_ENTRY)).toContain('export default wrapServerEntryWithSentryTunnel(entry, undefined, "");'); + expect(await load(USER_ENTRY)).toContain('setTunnelRoute(undefined, "");'); expect(await load(`${USER_ENTRY}?sentry-tunnel-original`)).toBeNull(); expect(await load('/app/node_modules/@tanstack/react-start/dist/default-entry/esm/server.js')).toContain( - 'wrapServerEntryWithSentryTunnel', + 'setTunnelRoute(', ); expect( await load('C:\\app\\node_modules\\@tanstack\\react-start\\dist\\plugin\\default-entry\\server.ts'), - ).toContain('wrapServerEntryWithSentryTunnel'); + ).toContain('setTunnelRoute('); expect(await load('/app/src/routes/index.tsx')).toBeNull(); }); @@ -112,25 +112,22 @@ describe('server entry tunnel vite plugin', () => { const load = setup(); expect(await load(USER_ENTRY)).toContain(`export * from "${USER_ENTRY}?sentry-tunnel-original";`); + expect(await load(USER_ENTRY)).toContain(`export { default } from "${USER_ENTRY}?sentry-tunnel-original";`); }); it('passes the build-time options when `tunnelRoute` derives the path from the DSN', async () => { - expect(await setup(true)(USER_ENTRY)).toContain('wrapServerEntryWithSentryTunnel(entry, {}, "");'); + expect(await setup(true)(USER_ENTRY)).toContain('setTunnelRoute({}, "");'); expect(await setup({ allowedDsns: ['https://public@o0.ingest.sentry.io/0'] })(USER_ENTRY)).toContain( - 'wrapServerEntryWithSentryTunnel(entry, {"allowedDsns":["https://public@o0.ingest.sentry.io/0"]}, "");', + 'setTunnelRoute({"allowedDsns":["https://public@o0.ingest.sentry.io/0"]}, "");', ); }); - it('serves a static `tunnelRoute` path from the wrapper too', async () => { - expect(await setup('/monitor')(USER_ENTRY)).toContain( - 'wrapServerEntryWithSentryTunnel(entry, {"path":"/monitor"}, "");', - ); + it('passes a static `tunnelRoute` path too', async () => { + expect(await setup('/monitor')(USER_ENTRY)).toContain('setTunnelRoute({"path":"/monitor"}, "");'); }); - it('passes the base path Start routes on to the wrapper', async () => { - expect(await setup(undefined, '/app')(USER_ENTRY)).toContain( - 'wrapServerEntryWithSentryTunnel(entry, undefined, "/app");', - ); + it('passes the base path Start routes on', async () => { + expect(await setup(undefined, '/app')(USER_ENTRY)).toContain('setTunnelRoute(undefined, "/app");'); }); it('imports a bundled SDK from where the app resolves it, which also works without hoisting', async () => { @@ -138,7 +135,7 @@ describe('server entry tunnel vite plugin', () => { const load = setup(undefined, '', resolve); expect(await load('/app/node_modules/@tanstack/react-start/dist/default-entry/esm/server.js')).toContain( - `import { _INTERNAL_wrapServerEntryWithSentryTunnel as wrapServerEntryWithSentryTunnel } from "${SDK_ID}";`, + `import { _INTERNAL_setTunnelRoute as setTunnelRoute } from "${SDK_ID}";`, ); expect(resolve).toHaveBeenCalledWith('@sentry/tanstackstart-react', '/app/package.json', { skipSelf: true }); }); @@ -183,7 +180,7 @@ describe('server entry tunnel vite plugin in `vite dev`', () => { afterEach(() => { getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); }); /** Sets a client, with `serveTunnelIntegration` set up when `managedTunnel` is given. */ diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 3afe8c68ed3b..627eccbc5372 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -16,7 +16,7 @@ "typescript": "~5.8.0" }, "scripts": { - "clean": "pnpm exec rimraf sentry-typescript-*.tgz", - "build:tarball": "npm pack" + "clean": "node ../../scripts/rm.mjs sentry-typescript-*.tgz", + "build:tarball": "pnpm pack" } } diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 9d9e62bd1574..d6cf352395ee 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -49,9 +49,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-vercel-edge-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-vercel-edge-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 331191e40d40..371c338fe7db 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -40,8 +40,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-vite-plugin-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-vite-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", "test": "vitest run", diff --git a/packages/vue/package.json b/packages/vue/package.json index d18f6c9e213f..d6d351d77373 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -46,7 +46,7 @@ "dependencies": { "@sentry/browser": "11.11.0", "@sentry/core": "11.11.0", - "@sentry/conventions": "^0.30.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "@tanstack/vue-router": "^1.64.0", @@ -76,9 +76,9 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.ts && madge --circular src/tanstackrouter.ts", - "clean": "rimraf build coverage sentry-vue-*.tgz", + "build:tarball": "pnpm pack", + "circularDepCheck": "madge --circular src/index.ts src/tanstackrouter.ts", + "clean": "node ../../scripts/rm.mjs build coverage sentry-vue-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 9a35d69429dc..6afdabd89b4e 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -48,11 +48,11 @@ "build:bundle:watch": "rollup --config rollup.bundle.config.mjs --watch", "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", + "build:tarball": "pnpm pack", "test": "vitest run", "test:watch": "vitest --watch", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-wasm-*.tgz", + "clean": "node ../../scripts/rm.mjs build coverage sentry-wasm-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 3579068d871d..a152977b26b6 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -45,8 +45,8 @@ "build:watch": "run-p build:transpile:watch", "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", - "build:tarball": "npm pack", - "clean": "rimraf build coverage sentry-webpack-plugin-*.tgz", + "build:tarball": "pnpm pack", + "clean": "node ../../scripts/rm.mjs build coverage sentry-webpack-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", "test": "vitest run", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a219ba940d1d..cf8b8ac67374 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,7 +206,7 @@ importers: version: 12.1.0(size-limit@12.1.0(jiti@2.7.0)) '@size-limit/webpack': specifier: ~12.1.0 - version: 12.1.0(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(size-limit@12.1.0(jiti@2.7.0))(svgo@4.1.0)(uglify-js@3.13.3) + version: 12.1.0(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(size-limit@12.1.0(jiti@2.7.0))(svgo@4.1.0)(uglify-js@3.13.3) '@types/jsdom': specifier: ^21.1.6 version: 21.1.6 @@ -215,7 +215,7 @@ importers: version: 20.19.43 '@vitest/coverage-v8': specifier: ^4.1.11 - version: 4.1.11(vitest@4.1.11) + version: 4.1.11(vitest@4.1.12) deepmerge: specifier: ^4.2.2 version: 4.3.1 @@ -238,8 +238,8 @@ importers: specifier: ^6.2.0 version: 6.2.0 nx: - specifier: 22.7.10 - version: 22.7.10 + specifier: 24.0.3 + version: 24.0.3 oxfmt: specifier: ^0.74.0 version: 0.74.0 @@ -249,9 +249,6 @@ importers: oxlint-tsgolint: specifier: ^7.0.2004 version: 7.0.2004 - rimraf: - specifier: ^6.1.3 - version: 6.1.3 rollup: specifier: ^4.60.3 version: 4.62.3 @@ -274,11 +271,11 @@ importers: specifier: ~7.0.2 version: 7.0.2 vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) webpack: specifier: ^5.112.2 - version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) yalc: specifier: ^1.0.0-pre.53 version: 1.0.0-pre.53 @@ -301,8 +298,8 @@ importers: specifier: 11.11.0 version: link:../../packages/browser-utils '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -313,11 +310,11 @@ importers: specifier: 11.11.0 version: link:../../packages/replay-canvas '@sentry/rrweb': - specifier: 2.45.0 - version: 2.45.0 + specifier: 2.46.0 + version: 2.46.0 '@sentry/rrweb-types': - specifier: ^2.45.0 - version: 2.45.0 + specifier: ^2.46.0 + version: 2.46.0 '@sentry/wasm': specifier: 11.11.0 version: link:../../packages/wasm @@ -332,7 +329,7 @@ importers: version: 1.21.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0) babel-loader: specifier: ^10.1.1 - version: 10.1.1(@babel/core@7.29.7(supports-color@10.0.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + version: 10.1.1(@babel/core@7.29.7(supports-color@10.0.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) fflate: specifier: 0.8.3 version: 0.8.3 @@ -341,19 +338,16 @@ importers: version: 14.0.3 html-webpack-plugin: specifier: ^5.5.0 - version: 5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + version: 5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) webpack: specifier: ^5.112.2 - version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) dev-packages/bun-integration-tests: devDependencies: '@sentry-internal/node-integration-tests': specifier: 11.11.0 version: link:../node-integration-tests - '@sentry-internal/test-utils': - specifier: 11.11.0 - version: link:../test-utils '@sentry/bun': specifier: 11.11.0 version: link:../../packages/bun @@ -382,26 +376,23 @@ importers: specifier: 8.23.1 version: 8.23.1 vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) dev-packages/bundler-plugin-integration-tests: devDependencies: '@sentry/bundler-plugins': specifier: 11.11.0 version: link:../../packages/bundler-plugins - premove: - specifier: ^4.0.0 - version: 4.0.0 sentry: - specifier: ^0.45.0 - version: 0.45.0 + specifier: ^0.49.0 + version: 0.49.0 typescript: specifier: ~6.0.3 version: 6.0.3 vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) dev-packages/bundler-tests: devDependencies: @@ -458,13 +449,13 @@ importers: version: 4.62.3 vite: specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) webpack: specifier: ^5.112.2 - version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) dev-packages/clear-cache-gh-action: devDependencies: @@ -482,7 +473,7 @@ importers: version: 0.133.0(zod@4.6.7) '@cloudflare/vite-plugin': specifier: 1.34.0 - version: 1.34.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(workerd@1.20260426.1)(wrangler@4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43)) + version: 1.34.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(workerd@1.20260426.1)(wrangler@4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43)) '@cloudflare/workers-types': specifier: ^4.20260426.0 version: 4.20260519.1 @@ -494,16 +485,16 @@ importers: version: 1.52.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0) '@langchain/core': specifier: ^1.2.17 - version: 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + version: 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) '@langchain/langgraph': specifier: ^1.0.1 - version: 1.4.21(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.6.7) + version: 1.4.21(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.6.7) '@langchain/openai': specifier: ^1.6.2 - version: 1.6.2(@aws-sdk/credential-provider-node@3.972.92)(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.1) + version: 1.6.2(@aws-sdk/credential-provider-node@3.972.92)(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.2) '@langchain/typesafe': specifier: ^0.0.2 - version: 0.0.2(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(langchain@1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.1)) + version: 0.0.2(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(langchain@1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.2)) '@opentelemetry/api': specifier: ^1.9.1 version: 1.10.0 @@ -520,8 +511,8 @@ importers: specifier: 11.11.0 version: link:../../packages/cloudflare '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -534,21 +525,18 @@ importers: hono: specifier: ^4.13.7 version: 4.13.11 - langchain: - specifier: ^1.5.16 - version: 1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.1) openai: specifier: 5.18.1 - version: 5.18.1(ws@8.22.1)(zod@4.6.7) + version: 5.18.1(ws@8.22.2)(zod@4.6.7) prisma: specifier: 6.15.0 version: 6.15.0(magicast@0.3.5)(typescript@7.0.2) vite: specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) wrangler: specifier: 4.156.0 version: 4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43) @@ -559,8 +547,8 @@ importers: specifier: 11.11.0 version: link:../node-integration-tests '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -574,14 +562,10 @@ importers: specifier: ^8.23.1 version: 8.23.1 vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - dev-packages/e2e-tests: - devDependencies: - rimraf: - specifier: ^6.1.3 - version: 6.1.3 + dev-packages/e2e-tests: {} dev-packages/external-contributor-gh-action: devDependencies: @@ -629,13 +613,13 @@ importers: version: 1.4.0 '@earendil-works/pi-ai': specifier: ^1.4.0 - version: 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + version: 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) '@earendil-works/pi-durable': specifier: ^1.4.0 - version: 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + version: 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) '@flue/runtime': specifier: 2.3.0 - version: 2.3.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(typescript@7.0.2)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + version: 2.3.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(typescript@7.0.2)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) '@google/genai': specifier: ^1.20.0 version: 1.52.0(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(supports-color@10.0.0) @@ -656,28 +640,28 @@ importers: version: 12.0.2(supports-color@10.0.0) '@langchain/anthropic': specifier: ^0.3.10 - version: 0.3.31(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(zod@4.6.7) + version: 0.3.31(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(zod@4.6.7) '@langchain/core': specifier: ^0.3.80 - version: 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) + version: 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) '@langchain/langgraph': specifier: ^0.2.32 - version: 0.2.74(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod-to-json-schema@3.25.2(zod@4.6.7)) + version: 0.2.74(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod-to-json-schema@3.25.2(zod@4.6.7)) '@langchain/openai': specifier: ^0.5.0 - version: 0.5.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(ws@8.22.1) + version: 0.5.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(ws@8.22.2) '@mistralai/mistralai': specifier: 3.3.0 version: 3.3.0(@aws-sdk/client-s3@3.1161.0)(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@cfworker/json-schema@4.1.1)(@opentelemetry/api@1.9.0)(supports-color@10.0.0) '@modelcontextprotocol/client': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.4.0 + version: 2.4.0 '@modelcontextprotocol/sdk': specifier: 1.32.2 version: 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) '@modelcontextprotocol/server': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.4.0 + version: 2.4.0 '@neondatabase/serverless': specifier: 1.2.0 version: 1.2.0 @@ -689,7 +673,7 @@ importers: version: 6.15.0(prisma@6.15.0(magicast@0.3.5)(typescript@7.0.2))(typescript@7.0.2) '@prisma/orm-postgres': specifier: 8.0.0-rc.17 - version: 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@sentry-internal/rollup-utils': specifier: 11.11.0 version: link:../rollup-utils @@ -700,8 +684,8 @@ importers: specifier: 11.11.0 version: link:../../packages/aws-serverless '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -720,6 +704,9 @@ importers: '@sentry/server-utils': specifier: 11.11.0 version: link:../../packages/server-utils + '@smithy/core': + specifier: 3.38.1 + version: 3.38.1 '@smithy/node-http-handler': specifier: 4.12.1 version: 4.12.1 @@ -739,8 +726,8 @@ importers: specifier: ^0.10.9 version: 0.10.9 body-parser: - specifier: ^2.3.0 - version: 2.3.0(supports-color@10.0.0) + specifier: ^2.4.0 + version: 2.4.0(supports-color@10.0.0) bullmq: specifier: ^5.79.1 version: 5.81.5(redis@5.12.1(@opentelemetry/api@1.9.0))(supports-color@10.0.0) @@ -760,8 +747,8 @@ importers: specifier: 0.28.1 version: 0.28.1 express: - specifier: ^4.21.2 - version: 4.22.1(supports-color@10.0.0) + specifier: ^4.22.3 + version: 4.22.3(supports-color@10.0.0) fastify: specifier: ^5.12.7 version: 5.12.7 @@ -830,7 +817,7 @@ importers: version: 2.1.2 openai: specifier: 5.18.1 - version: 5.18.1(ws@8.22.1)(zod@4.6.7) + version: 5.18.1(ws@8.22.2)(zod@4.6.7) pg: specifier: 8.23.1 version: 8.23.1 @@ -884,7 +871,7 @@ importers: version: 4.9.0 ws: specifier: ^8.20.1 - version: 8.22.1 + version: 8.22.2 yargs: specifier: ^18.2.0 version: 18.2.0 @@ -930,11 +917,11 @@ importers: dev-packages/test-utils: dependencies: express: - specifier: ^4.21.2 - version: 4.22.1(supports-color@10.0.0) + specifier: ^4.22.3 + version: 4.22.3(supports-color@10.0.0) ws: specifier: ^8.20.1 - version: 8.22.1 + version: 8.22.2 devDependencies: '@playwright/test': specifier: 1.64.1 @@ -958,8 +945,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -969,7 +956,7 @@ importers: devDependencies: '@angular-devkit/build-angular': specifier: ^14.2.13 - version: 14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(chokidar@3.6.0)(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(debug@4.4.3(supports-color@10.0.0))(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(lightningcss@1.33.0)(ng-packagr@14.3.0(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(@types/node@14.18.63)(supports-color@10.0.0)(tslib@2.8.1)(typescript@4.6.4))(supports-color@10.0.0)(typescript@4.6.4)(uglify-js@3.13.3) + version: 14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(chokidar@3.6.0)(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(debug@4.4.3(supports-color@10.0.0))(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(lightningcss@1.33.0)(ng-packagr@14.3.0(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(@types/node@14.18.63)(supports-color@10.0.0)(tslib@2.8.1)(typescript@4.6.4))(supports-color@10.0.0)(typescript@4.6.4)(uglify-js@3.13.3) '@angular/cli': specifier: ^14.2.13 version: 14.2.13(chokidar@3.6.0)(supports-color@10.0.0) @@ -1019,8 +1006,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1041,14 +1028,14 @@ importers: specifier: ^4.16.19 version: 4.16.19(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(typescript@7.0.2) vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/aws-serverless: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1081,8 +1068,8 @@ importers: specifier: 11.11.0 version: link:../browser-utils '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1109,8 +1096,8 @@ importers: packages/browser-utils: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1128,8 +1115,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1165,11 +1152,8 @@ importers: specifier: ^0.152.0 version: 0.152.0 sentry: - specifier: ^0.45.0 - version: 0.45.0 - supports-color: - specifier: ^8.1.1 - version: 8.1.1 + specifier: ^0.49.0 + version: 0.49.0 devDependencies: '@babel/core': specifier: ^7.18.5 @@ -1192,18 +1176,18 @@ importers: '@types/node': specifier: ^20.19.0 version: 20.19.43 - premove: - specifier: ^4.0.0 - version: 4.0.0 rolldown: specifier: ^1.0.0 version: 1.2.16 + vite: + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vitest: - specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@8.1.1))(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@8.1.1))(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) webpack: specifier: 5.112.2 - version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) packages/cloudflare: dependencies: @@ -1211,8 +1195,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1239,8 +1223,8 @@ importers: specifier: ^8.14.0 version: 8.18.0 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) wrangler: specifier: 4.156.0 version: 4.156.0(@cloudflare/workers-types@5.20260710.1)(@types/node@20.19.43) @@ -1248,8 +1232,8 @@ importers: packages/core: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 devDependencies: '@sentry-internal/rollup-utils': specifier: 11.11.0 @@ -1264,8 +1248,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1286,8 +1270,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1297,7 +1281,7 @@ importers: devDependencies: '@effect/vitest': specifier: ^4.0.0 - version: 4.0.0(effect@4.0.0)(vitest@4.1.11) + version: 4.0.0(effect@4.0.0)(vitest@4.1.12) '@sentry-internal/rollup-utils': specifier: 11.11.0 version: link:../../dev-packages/rollup-utils @@ -1311,8 +1295,8 @@ importers: specifier: 11.11.0 version: link:../bun '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1339,8 +1323,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1366,15 +1350,12 @@ importers: '@embroider/addon-dev': specifier: ^8.3.0 version: 8.3.1(@glint/template@1.8.0)(rollup@4.62.3)(supports-color@10.0.0) - '@embroider/compat': - specifier: ^4.1.17 - version: 4.1.22(@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0) '@embroider/core': specifier: ^4.4.7 version: 4.6.3(@glint/template@1.8.0)(supports-color@10.0.0) '@embroider/vite': specifier: ^1.7.2 - version: 1.7.9(@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 1.7.9(@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@glint/ember-tsc': specifier: ^1.5.0 version: 1.10.0(ember-source@6.12.0(rsvp@4.8.5)(supports-color@10.0.0))(supports-color@10.0.0)(typescript@5.8.3) @@ -1388,8 +1369,8 @@ importers: specifier: ~5.8.0 version: 5.8.3 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/esbuild-plugin: dependencies: @@ -1472,9 +1453,6 @@ importers: '@sentry-internal/rollup-utils': specifier: 11.11.0 version: link:../../dev-packages/rollup-utils - '@testing-library/react': - specifier: ^15.0.5 - version: 15.0.5(@types/react@17.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -1483,13 +1461,13 @@ importers: version: 18.3.1(react@18.3.1) webpack: specifier: ^5.112.2 - version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) packages/google-cloud-serverless: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1528,8 +1506,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1564,9 +1542,6 @@ importers: hono-5: specifier: npm:hono@5.0.0-rc.1 version: hono@5.0.0-rc.1 - wrangler: - specifier: 4.156.0 - version: 4.156.0(@cloudflare/workers-types@4.20250922.0)(@types/node@20.19.43) packages/integration-shims: dependencies: @@ -1581,8 +1556,8 @@ importers: packages/nestjs: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1627,8 +1602,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1679,8 +1654,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1702,13 +1677,10 @@ importers: version: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) nitro: specifier: ^3.0.260415-beta - version: 3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) srvx: specifier: ^0.11.22 version: 0.11.22 - unstorage: - specifier: ^2.0.0-alpha.7 - version: 2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1) packages/node: dependencies: @@ -1719,8 +1691,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1764,7 +1736,7 @@ importers: dependencies: '@nuxt/kit': specifier: ^4.6.0 - version: 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + version: 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) '@sentry/browser': specifier: 11.11.0 version: link:../browser @@ -1775,8 +1747,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1798,7 +1770,7 @@ importers: version: 4.20260519.1 '@nuxt/nitro-server': specifier: ^3.21.11 - version: 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3))(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + version: 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5))(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) '@nuxt/schema': specifier: ^3.21.11 version: 3.21.11 @@ -1813,22 +1785,22 @@ importers: version: 1.15.11 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) nitropack: specifier: ^2.13.4 - version: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + version: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) nuxi: specifier: ^3.25.1 version: 3.25.1 nuxt: specifier: 3.21.11 - version: 3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3) + version: 3.21.11(724864ca76bc73825b6249c96f34efd5) unstorage: specifier: ^1.17.5 version: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vue: specifier: ^3.5.41 version: 3.5.44(typescript@7.0.2) @@ -1836,8 +1808,8 @@ importers: packages/opentelemetry: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1852,8 +1824,8 @@ importers: packages/profiling-node: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1877,8 +1849,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1895,12 +1867,6 @@ importers: '@types/history-4': specifier: npm:@types/history@4.7.8 version: '@types/history@4.7.8' - '@types/history-5': - specifier: npm:@types/history@4.7.8 - version: '@types/history@4.7.8' - '@types/node-fetch': - specifier: ^2.6.11 - version: 2.6.13 '@types/react': specifier: 17.0.3 version: 17.0.3 @@ -1913,9 +1879,6 @@ importers: history-4: specifier: npm:history@4.6.0 version: history@4.6.0 - history-5: - specifier: npm:history@4.9.0 - version: history@4.9.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -1953,8 +1916,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1971,12 +1934,12 @@ importers: specifier: 11.11.0 version: link:../server-utils sentry: - specifier: ^0.44.0 - version: 0.44.0 + specifier: ^0.49.0 + version: 0.49.0 devDependencies: '@react-router/dev': specifier: ^7.18.4 - version: 7.18.4(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(react-router@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(wrangler@4.156.0(@cloudflare/workers-types@5.20260710.1)(@types/node@20.19.43))(yaml@2.9.0) + version: 7.18.4(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(react-router@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(wrangler@4.156.0(@cloudflare/workers-types@5.20260710.1)(@types/node@20.19.43))(yaml@2.9.0) '@react-router/node': specifier: ^7.15.0 version: 7.18.4(react-router@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@6.0.3) @@ -1999,8 +1962,8 @@ importers: specifier: ^7.18.4 version: 7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/remix: dependencies: @@ -2014,8 +1977,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2032,8 +1995,8 @@ importers: specifier: 11.11.0 version: link:../server-utils sentry: - specifier: ^0.44.0 - version: 0.44.0 + specifier: ^0.49.0 + version: 0.49.0 yargs: specifier: ^18.2.0 version: 18.2.0 @@ -2053,9 +2016,6 @@ importers: '@sentry-internal/rollup-utils': specifier: 11.11.0 version: link:../../dev-packages/rollup-utils - '@types/express': - specifier: ^4.17.14 - version: 4.17.21 '@types/react': specifier: 17.0.3 version: 17.0.3 @@ -2072,8 +2032,8 @@ importers: specifier: ^7.8.5 version: 7.8.5 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/replay-canvas: dependencies: @@ -2088,8 +2048,8 @@ importers: specifier: 11.11.0 version: link:../../dev-packages/rollup-utils '@sentry/rrweb': - specifier: 2.45.0 - version: 2.45.0 + specifier: 2.46.0 + version: 2.46.0 packages/replay-internal: dependencies: @@ -2100,9 +2060,6 @@ importers: specifier: 11.11.0 version: link:../core devDependencies: - '@babel/core': - specifier: ^7.29.6 - version: 7.29.7(supports-color@10.0.0) '@sentry-internal/replay-worker': specifier: 11.11.0 version: link:../replay-worker @@ -2110,14 +2067,14 @@ importers: specifier: 11.11.0 version: link:../../dev-packages/rollup-utils '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/rrweb': - specifier: 2.45.0 - version: 2.45.0 + specifier: 2.46.0 + version: 2.46.0 '@sentry/rrweb-snapshot': - specifier: 2.45.0 - version: 2.45.0 + specifier: 2.46.0 + version: 2.46.0 fflate: specifier: 0.8.3 version: 0.8.3 @@ -2178,15 +2135,15 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core devDependencies: '@apm-js-collab/code-transformer-bundler-plugins': specifier: ^0.7.5 - version: 0.7.5(esbuild@0.28.1)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + version: 0.7.5(esbuild@0.28.1)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) '@neondatabase/serverless': specifier: 1.2.0 version: 1.2.0 @@ -2200,11 +2157,11 @@ importers: specifier: ^6.1.4 version: 6.1.4 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) webpack: specifier: ^5.112.2 - version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + version: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) packages/solid: dependencies: @@ -2212,8 +2169,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2243,11 +2200,11 @@ importers: specifier: ^1.9.18 version: 1.9.18 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) packages/solidstart: dependencies: @@ -2255,8 +2212,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2290,7 +2247,7 @@ importers: version: 1.0.0(solid-js@1.9.18) '@solidjs/start': specifier: ^1.3.2 - version: 1.3.2(e4a8c665f4a8ec10ddcce54d0b28f89d) + version: 1.3.2(1649c7f604f12e5f9af57e229fd2f3f2) '@solidjs/testing-library': specifier: 0.8.5 version: 0.8.5(@solidjs/router@1.0.0(solid-js@1.9.18))(solid-js@1.9.18) @@ -2305,22 +2262,19 @@ importers: version: 20.19.43 nitropack: specifier: ^2.13.4 - version: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - node-mock-http: - specifier: ^1.0.4 - version: 1.0.4 + version: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) solid-js: specifier: ^1.9.18 version: 1.9.18 vinxi: specifier: ^0.5.11 - version: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) + version: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) packages/svelte: dependencies: @@ -2328,8 +2282,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2342,7 +2296,7 @@ importers: version: link:../../dev-packages/rollup-utils '@sveltejs/vite-plugin-svelte': specifier: 1.4.0 - version: 1.4.0(supports-color@10.0.0)(svelte@3.59.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 1.4.0(supports-color@10.0.0)(svelte@3.59.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@testing-library/svelte': specifier: ^3.2.1 version: 3.2.1(svelte@3.59.2) @@ -2350,8 +2304,8 @@ importers: specifier: 3.59.2 version: 3.59.2 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/sveltekit: dependencies: @@ -2365,8 +2319,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2403,10 +2357,10 @@ importers: version: link:../browser '@sveltejs/kit': specifier: ^2.70.2 - version: 2.70.2(@opentelemetry/api@1.10.0)(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(svelte@4.2.8)(typescript@6.0.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 2.70.2(@opentelemetry/api@1.10.0)(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(svelte@4.2.8)(typescript@6.0.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: ^3.0.0 - version: 3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + version: 3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@types/node': specifier: ^20.19.0 version: 20.19.43 @@ -2414,8 +2368,8 @@ importers: specifier: ^4.2.8 version: 4.2.8 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/tanstackstart-react: dependencies: @@ -2429,8 +2383,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2454,8 +2408,8 @@ importers: specifier: ^20.19.0 version: 20.19.43 vite: - specifier: ^8.3.5 - version: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + specifier: ^8.3.6 + version: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) packages/typescript: {} @@ -2497,8 +2451,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -3487,8 +3441,8 @@ packages: peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-proposal-decorators@7.24.7': - resolution: {integrity: sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==} + '@babel/plugin-proposal-decorators@7.29.7': + resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -3599,8 +3553,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-decorators@7.25.9': - resolution: {integrity: sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==} + '@babel/plugin-syntax-decorators@7.29.7': + resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -4096,9 +4050,6 @@ packages: resolution: {integrity: sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.12.18': - resolution: {integrity: sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==} - '@babel/runtime@7.18.9': resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} engines: {node: '>=6.9.0'} @@ -4160,10 +4111,18 @@ packages: '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + '@clack/core@1.5.1': resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==} engines: {node: '>= 20.12.0'} + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + '@clack/prompts@1.8.1': resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==} engines: {node: '>= 20.12.0'} @@ -4272,11 +4231,6 @@ packages: '@cloudflare/workers-types@5.20260710.1': resolution: {integrity: sha512-4ooaY2Pb5XGwDn8Fzm6jnTAJkIX0R5LBvL9euQpp2T58sQItlAQd9yivAlkwGhpY5cM1u81/9HaXwKAjXwtyzA==} - '@cnakazawa/watch@1.0.4': - resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} - engines: {node: '>=0.1.95'} - hasBin: true - '@colordx/core@5.5.0': resolution: {integrity: sha512-3PxTH8itZzltK0U9jTwVVnjLXvnDYuq3m+QXsHkENxWiPRh4WaoLcs1SQjqgZ55kS+QyirpH5BVwzP2gMVG6EQ==} @@ -4494,12 +4448,6 @@ packages: resolution: {integrity: sha512-vXjhIOtvrns9IBFlgEN1imSeXtuVIlLypaDexu7xzsvB4Ef6yruhH33DNd9OmO0nXIB4SbwdkIwEsLYnhrujqA==} engines: {node: 12.* || 14.* || >= 16} - '@embroider/compat@4.1.22': - resolution: {integrity: sha512-MmR77lOY3VQu0poeZH1RqwfNv/FoWXOk3aZ4+X8gy8ReDXQ/BTzROTbsm6pxc2DTalSrtHexHhRWS+Ppl8Ynyw==} - engines: {node: '>= 20.19.*'} - peerDependencies: - '@embroider/core': ^4.6.3 - '@embroider/core@4.6.3': resolution: {integrity: sha512-BdUDoifoB6YDJz30H+GNvQcEAB6a8/3OQinS+5wP3LlTCfPQjHChtZxdMwqvzReOSU1qVi2WGDx4xJh/0OREjg==} engines: {node: '>= 20.19.*'} @@ -6076,10 +6024,18 @@ packages: resolution: {integrity: sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==} engines: {node: '>=20'} + '@modelcontextprotocol/client@2.4.0': + resolution: {integrity: sha512-LgrD2OphHuG12vfKFKz6SOnq7bKHQmhGJIAYdD+MZBn2npHEyry6uD8zvktodtl5CQM85MO0anUyD9JwmUmxww==} + engines: {node: '>=20'} + '@modelcontextprotocol/core@2.0.0': resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} engines: {node: '>=20'} + '@modelcontextprotocol/core@2.4.0': + resolution: {integrity: sha512-z8QBKqdfXLPzkLQuG4xp8hrVEMm1PZ1IgxMN2sMbJiFiAKOWXnAzogXn2W4aqIBlP1ea6U5yiqVquvUNM1Cj6Q==} + engines: {node: '>=20'} + '@modelcontextprotocol/sdk@1.32.2': resolution: {integrity: sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==} engines: {node: '>=18'} @@ -6090,8 +6046,8 @@ packages: '@cfworker/json-schema': optional: true - '@modelcontextprotocol/server@2.0.0': - resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} + '@modelcontextprotocol/server@2.4.0': + resolution: {integrity: sha512-rgR9yxSaAJ+bcsi/XroT/S2AfPiIWloXAnrdfYSCms9ivVju0ishu/wW0J21Ho6+ZOR1r7RyeRbjiRuF87ix5Q==} engines: {node: '>=20'} '@mongodb-js/saslprep@1.4.6': @@ -6388,57 +6344,57 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true - '@nx/nx-darwin-arm64@22.7.10': - resolution: {integrity: sha512-gy0ASfsnFsMmTLQaGTQyf8l95kAgawBpdMrgtxsnXzyKAasqzfIEhEbb74/6+DmsmcnNyfBvAKvVvFBiNs3ZYw==} + '@nx/nx-darwin-arm64@24.0.3': + resolution: {integrity: sha512-vwmDvuuvsn2asD55wFBjpvp01eAUJVI47j7+MyLvFUn//IAClvVFIqjCYkMOe1gu0cJBfFq3QB8X5lv61/h8GA==} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@22.7.10': - resolution: {integrity: sha512-IGNSVU85ktrNm9cH9yLgSaQjmRVCcEldaZwtAqNaMcNU+Go5xY4zYv8tDQ2MJ3hXpi6pNdcFzZp8tZIJliHyPA==} + '@nx/nx-darwin-x64@24.0.3': + resolution: {integrity: sha512-eO9CI3Mkm/iNSh6muU1mrcdv/Wo7iwNbezZIrsg1G6qRmyeRvcaepYdWmWzqu6m7ioV48vTfmtijQvDAjDkV/g==} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@22.7.10': - resolution: {integrity: sha512-KlmBmvD8uE7Saah3DO61D7ywn08FtYJE0I0AwZGs+0cisY1bUawtZZNYvbPTprHaCOWvbU2GeIVstK34vmbVXg==} + '@nx/nx-freebsd-x64@24.0.3': + resolution: {integrity: sha512-DivUPH4rx82Tr51kiIJ/Tzg0Wq3qEMbvKPpJ7gfyu5eGU7r2+0EOwH6R1c6mleoLJXRQN6hyPC7b794Tcnn9WA==} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@22.7.10': - resolution: {integrity: sha512-xwCv9gR968hMeeWUqBneLR9nBqP+ECPVFumdWCxMBh116ftCjMMik8E3BdUKLdqreVYFJdwvJRPortHj/YYikA==} + '@nx/nx-linux-arm-gnueabihf@24.0.3': + resolution: {integrity: sha512-SEyVU+fFboOrLjl7YFytmCUUy98YaMXtlIVnmRg994BhYnR0VKrFiwiSAFnCkxmBJHlAiU/79bGp38rc/DPyuw==} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@22.7.10': - resolution: {integrity: sha512-AiwBgerLicRzcyEevqwDuP22Nj5nFFrjJsCeZ+D4rC2HmS4LdBzXb+BBuHAZTysCo9QClk1t9VuVwwKyorU7ZA==} + '@nx/nx-linux-arm64-gnu@24.0.3': + resolution: {integrity: sha512-azWQvnXMUOO0Rls1kID2AdQ/0Iek2/CwyKVJ+JlI0DBVcYTAEbj4azTTtxCn+lGtIx+bVS1Q6oXXMjiu9e3KZg==} cpu: [arm64] os: [linux] libc: [glibc] - '@nx/nx-linux-arm64-musl@22.7.10': - resolution: {integrity: sha512-8GNi8v6Ej/TyAaVBiFRAwFYrqdje/dyFbPzpq895m3ow8zbPrYmoz/qizrgkRzBFulgkSxu33LXi388reVLi2w==} + '@nx/nx-linux-arm64-musl@24.0.3': + resolution: {integrity: sha512-dtg+v3O0TMXjbVJHw3hmyz98WxJl3sf4xvFDonjrKJzxYvesNYMczpK+Blbok/eQwe9YgMF0oEhuPC9Kn1pFEg==} cpu: [arm64] os: [linux] libc: [musl] - '@nx/nx-linux-x64-gnu@22.7.10': - resolution: {integrity: sha512-spcDvoQK/mrSvnHFdophlwgzUoa8lYXAZTk15+w/gLpaowP5hR4mInhUWsU9VuEoe41J3x+FcFwmw01Djj6KNQ==} + '@nx/nx-linux-x64-gnu@24.0.3': + resolution: {integrity: sha512-167H3rRmdT/5wFQhMDN6hNNNKSlawqDaj+6AIj2in0RDUNShsW4Vp5LH15ey6y8VxlUdYQCcJ/EISvD6OoBT6A==} cpu: [x64] os: [linux] libc: [glibc] - '@nx/nx-linux-x64-musl@22.7.10': - resolution: {integrity: sha512-76PdpGIQ1PVA77sYM4Ons779Yx7y3H+Hlsax2HhifAv8dKhAz9/Lmhst9ZqS2zU5v03lGEnN77W4KZQHJBiltA==} + '@nx/nx-linux-x64-musl@24.0.3': + resolution: {integrity: sha512-VUN0YKcDyI5J8f31IMhy1VTsn4TgFwalYsD97DYyobcnG6tWJyT68M6tXnm4X1r5kd9/v7B4sZxkU+1Sy9QEyQ==} cpu: [x64] os: [linux] libc: [musl] - '@nx/nx-win32-arm64-msvc@22.7.10': - resolution: {integrity: sha512-dARAojqVyvE88Mt1ms6+UTb3TIAoo4xzkPaX05Qcmh/c8+SQgA24JuqiRp+4kRz1voYwFexRpWpcrfpKJ0Nr+Q==} + '@nx/nx-win32-arm64-msvc@24.0.3': + resolution: {integrity: sha512-w+J6bbeKKPgq/mKXSSFzXr2sCqvD0WY/HtzB+JC/xeXyQwiwjUe2/Jnkn3ceizPRk4rkWQ97DK3bgJimZFZYXw==} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@22.7.10': - resolution: {integrity: sha512-qlH70TBMrRw/5pMg6MtPXkuM3LzDHszpqWcJ4NPdipLsyD6T+B+NteanlPtxZrdfqi+8Hd/7fv2OkllA8iY1fg==} + '@nx/nx-win32-x64-msvc@24.0.3': + resolution: {integrity: sha512-8JL0Q8Uhkqrc3850qpGFFUl/Q16SIdSJHB2oW2IhPFwkq2AM2MNGONu5Zud5HJsSoJhuAV0zrH0wJQ5odMjylg==} cpu: [x64] os: [win32] @@ -8120,8 +8076,8 @@ packages: resolution: {integrity: sha512-MLxTpTU3E8QACQ/5c0sENMR2gRiMXpGaKeD5IHY+3wyU2fUSJVB0QPU/l1WhoyZbX8N9ospBgf5UEG7taVF9rg==} engines: {node: ^14.15.0 || >=16.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@sentry/conventions@0.30.0': - resolution: {integrity: sha512-WTumd5A2iFmHTRubi6mc6gLHU8LswPQqoNaZu6T1cxiTUmy3WdEw0i8FAnVbcgxTlsumX5oh63/b4B9lY5wAPQ==} + '@sentry/conventions@0.32.0': + resolution: {integrity: sha512-WkSQcDhUUsz1SwxR23LpmyPKba+dgxxiH0DWV4VSM9yaAvhhl87ppW2Ts3g4piIKCVflc31aJeWy+biUbYfsMQ==} engines: {node: '>=14'} '@sentry/node-cpu-profiler@2.4.4': @@ -8132,17 +8088,17 @@ packages: resolution: {integrity: sha512-tF8eI4Z5YP6l7piLcy4BeGv6J96Y2ttssAAVFPgdzxzsT9i+S2olqGWgA9qav6+Gzs6E5pC9PAkObp2eDftabQ==} engines: {node: '>=18'} - '@sentry/rrdom@2.45.0': - resolution: {integrity: sha512-JIDMWjghWbV6xlqKHiUFAInOLMYClrE1Sm5OoACQk1Bfv0+Am/Bgnb7Hj/uA2Quh8uKybn0kh9PqQ/AV3r3rzg==} + '@sentry/rrdom@2.46.0': + resolution: {integrity: sha512-Ii5Oj2/eRo/cCi/4CmXHViscTFuAcAfV2icR4r+/d6HXC2/V48eejux5GZbcjxPipHAWfFXOTtm3rxBCtDtDaQ==} - '@sentry/rrweb-snapshot@2.45.0': - resolution: {integrity: sha512-me5T/2e58Lic5ztB9HSrko2m8gnCgDgy2Qq5iW0JbMX1FiztuwxqKku3/ZxhK10iVIgZGDqzT/d7EoJDPrC1qw==} + '@sentry/rrweb-snapshot@2.46.0': + resolution: {integrity: sha512-Z9BTdBdIiQMNLBvCTvIx0vVSnH01nscK2zZ8UJSxncf9HcJ3ox0fjZ2+h6XBmSxJo0VK95qrXCeBmU9X27X/4w==} - '@sentry/rrweb-types@2.45.0': - resolution: {integrity: sha512-IeCGojaSHZUgstG3nrTE5KJQkd4OvseYqsvCn+Nt18r8IkluaIiJhYstISPU8OaizZM1V5GM6DFV93Xweyqa4A==} + '@sentry/rrweb-types@2.46.0': + resolution: {integrity: sha512-U+kgUzIR4QkucJaU9CfL0YvOStfNthA8auoymcd7NeAXmN0xdEOAq+C/oxqawf7PqEk5XTpWeD160giqfgRtJw==} - '@sentry/rrweb@2.45.0': - resolution: {integrity: sha512-R5ukWK6E3dgt7eE47d2m5MHPsxGtLxn3DK/AD1K6edgizWXMSP8+aatmM9cW1ZTmJhSN5mXU5YRO7QvkvH8mEA==} + '@sentry/rrweb@2.46.0': + resolution: {integrity: sha512-8r7BcYy/wPo6gGSAjtx3uuVEGinXHYRyDtXGh80GXK48mRziV6c8iDj2LiE98V7ZY1YDx6Br6SdrU9CWEt4gxA==} '@shikijs/core@1.29.2': resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} @@ -8689,9 +8645,6 @@ packages: '@types/aws-lambda@8.10.164': resolution: {integrity: sha512-XOnrazWcOd6yWPnR7DxqCBPPYciDjHq+NN8LPwwqBCJNXLyPJgKo47fZpJ113+oyAn9Zti0Cf6H4k6yPcE3WFg==} - '@types/babel__code-frame@7.27.0': - resolution: {integrity: sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -8836,9 +8789,6 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} @@ -9262,11 +9212,11 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/expect@4.1.12': + resolution: {integrity: sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==} - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@4.1.12': + resolution: {integrity: sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -9279,18 +9229,24 @@ packages: '@vitest/pretty-format@4.1.11': resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + '@vitest/pretty-format@4.1.12': + resolution: {integrity: sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==} - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@vitest/runner@4.1.12': + resolution: {integrity: sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==} - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + '@vitest/snapshot@4.1.12': + resolution: {integrity: sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==} + + '@vitest/spy@4.1.12': + resolution: {integrity: sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==} '@vitest/utils@4.1.11': resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@vitest/utils@4.1.12': + resolution: {integrity: sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==} + '@volar/kit@2.4.28': resolution: {integrity: sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==} peerDependencies: @@ -9667,10 +9623,6 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@3.2.0: - resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} - engines: {node: '>=4'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -9679,18 +9631,6 @@ packages: resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} hasBin: true - ansi-html@0.0.9: - resolution: {integrity: sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==} - hasBin: true - - ansi-regex@3.0.1: - resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} - engines: {node: '>=4'} - - ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -9865,6 +9805,9 @@ packages: engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true + async-disk-cache@1.3.5: + resolution: {integrity: sha512-VZpqfR0R7CEOJZ/0FOTgWq70lCrZyS1rkI8PXugDUkTKyyAUgZ2zQ09gLhMkEn+wN8LYeUTPxZdXtlX/kmbXKQ==} + async-disk-cache@2.1.0: resolution: {integrity: sha512-iH+boep2xivfD9wMaZWkywYIURSmsL96d6MoqrC94BnGSvXE4Quf8hnJiHGFYhw/nLeIa1XyRaf4vvcvkwAefg==} engines: {node: 8.* || >= 10.*} @@ -9925,9 +9868,6 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} - axios@1.18.1: - resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} - axios@1.21.0: resolution: {integrity: sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==} @@ -9944,10 +9884,6 @@ packages: babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} - babel-import-util@2.1.1: - resolution: {integrity: sha512-3qBQWRjzP9NreSH/YrOEU1Lj5F60+pWSLP0kIdCWxjFHH7pX2YPHIxQ67el4gnMNfYoDxSDGcT0zpVlZ+gVtQA==} - engines: {node: '>= 12.*'} - babel-import-util@3.0.1: resolution: {integrity: sha512-2copPaWQFUrzooJVIVZA/Oppx/S/KOoZ4Uhr+XWEQDMZ8Rvq/0SNQpbdIyMBJ8IELWt10dewuJw+tX4XjOo7Rg==} engines: {node: '>= 12.*'} @@ -9978,12 +9914,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - babel-plugin-debug-macros@2.0.0: - resolution: {integrity: sha512-7ZaLtXIY01PAPhLyjV3OACePnl+X5iQO7F4O/sOJHTfMf/36zyu14uVUxNiZmOUntYBsyT/VVplf1LrH6NcwvQ==} - engines: {node: '>=16'} - peerDependencies: - '@babel/core': ^7.0.0 - babel-plugin-ember-data-packages-polyfill@0.1.2: resolution: {integrity: sha512-kTHnOwoOXfPXi00Z8yAgyD64+jdSXk3pknnS7NlqnCKAU6YDkXZ4Y7irl66kaZjZn0FBBt0P4YOZFZk85jYOww==} engines: {node: 6.* || 8.* || 10.* || >= 12.*} @@ -9992,10 +9922,6 @@ packages: resolution: {integrity: sha512-pJajN/DkQUnStw0Az8c6khVcMQHgzqWr61lLNtVeu0g61LRW0k9jyK7vaedrHDWGe/Qe8sxG5wpiyW9NsMqFzA==} engines: {node: 6.* || 8.* || >= 10.*} - babel-plugin-ember-template-compilation@2.4.1: - resolution: {integrity: sha512-n+ktQ3JeyWrpRutSyPn2PsHeH+A94SVm+iUoogzf9VUqpP47FfWem24gpQXhn+p6+x5/BpuFJXMLXWt7ZoYAKA==} - engines: {node: '>= 12.*'} - babel-plugin-ember-template-compilation@3.1.0: resolution: {integrity: sha512-kk7cGyblE9n4MB98rqw2wuUW7YLD5FM+Tr97gNSYL4e8DBMQndLuWaWNx1wfd7o00NjFhhoTR+HZs2nj23g2Lw==} engines: {node: '>= 18.*'} @@ -10047,18 +9973,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-syntax-dynamic-import@6.18.0: - resolution: {integrity: sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==} - babel-preset-solid@1.8.17: resolution: {integrity: sha512-s/FfTZOeds0hYxYqce90Jb+0ycN2lrzC7VP1k1JIn3wBqcaexDKdYi6xjB+hMNkL+Q6HobKbwsriqPloasR9LA==} peerDependencies: '@babel/core': ^7.0.0 - babylon@6.18.0: - resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} - hasBin: true - backburner.js@2.8.0: resolution: {integrity: sha512-zYXY0KvpD7/CWeOLF576mV8S+bQsaIoj/GNLXXB+Eb8SJcQy5lqSjkRrZ0MZhdKUs9QoqmGNIEIe3NQfGiiscQ==} @@ -10123,9 +10042,6 @@ packages: resolution: {integrity: sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==} engines: {node: '>=0.8'} - bind-decorator@1.0.11: - resolution: {integrity: sha512-yzkH0uog6Vv/vQ9+rhSKxecnqGUZHYncg7qS7voz3Q76+TAi1SGiOKk2mlOvusQnFz9Dc4BC/NMkeXu11YgjJg==} - bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -10147,12 +10063,12 @@ packages: blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + body-parser@1.20.9: + resolution: {integrity: sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + body-parser@2.4.0: + resolution: {integrity: sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==} engines: {node: '>=18'} bonjour-service@1.2.1: @@ -10181,10 +10097,6 @@ packages: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} - engines: {node: 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -10195,10 +10107,6 @@ packages: peerDependencies: '@babel/core': ^7.17.9 - broccoli-concat@4.2.5: - resolution: {integrity: sha512-dFB5ATPwOyV8S2I7a07HxCoutoq23oY//LhM6Mou86cWUTB174rND5aQLR7Fu8FjFFLxoTbkk7y0VPITJ1IQrw==} - engines: {node: 10.* || >= 12.*} - broccoli-debug@0.6.5: resolution: {integrity: sha512-RIVjHvNar9EMCLDW/FggxFRXqpjhncM/3qq87bn/y+/zR9tqEkHvTqbyOc4QnB97NO2m6342w4wGkemkaeOuWg==} @@ -10228,6 +10136,9 @@ packages: resolution: {integrity: sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==} engines: {node: 10.* || >= 12.*} + broccoli-persistent-filter@1.4.6: + resolution: {integrity: sha512-0RejLwoC95kv4kta8KAa+FmECJCK78Qgm8SRDEK7YyU0N9Cx6KpY3UCDy9WELl3mCXLN8TokNxc7/hp3lL4lfw==} + broccoli-persistent-filter@3.1.3: resolution: {integrity: sha512-Q+8iezprZzL9voaBsDY3rQVl7c7H5h+bvv8SpzCZXPZgfBFCbx7KFQ2c3rZR6lW5k4Kwoqt7jG+rZMUg67Gwxw==} engines: {node: 10.* || >= 12.*} @@ -10243,9 +10154,8 @@ packages: resolution: {integrity: sha512-ZbGVQjivWi0k220fEeIUioN6Y68xjMy0xiLAc0LdieHI99gw+tafU8w0CggBDYVNsJMKUr006AZaM7gNEwCxEg==} engines: {node: 8.* || 10.* || >= 12.*} - broccoli@4.0.0: - resolution: {integrity: sha512-p5el5/ig0QeRGFPkLMPdm7KblkTm44eicEWfwnRTz6hncghVuRZ0+XDAtCi7ynxobeE/mey5Q7lAulFkgNzxVA==} - engines: {node: '>= 20.19.*'} + broccoli-string-replace@0.1.2: + resolution: {integrity: sha512-QHESTrrrPlKuXQNWsvXawSQbV2g34wCZ5oKgd6bntdOuN8VHxbg1BCBHqVY5HxXJhWelimgGxj3vI7ECkyij8g==} browserslist-to-esbuild@2.1.1: resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} @@ -10259,9 +10169,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - bson@1.1.6: resolution: {integrity: sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==} engines: {node: '>=0.6.19'} @@ -10416,10 +10323,6 @@ packages: caniuse-lite@1.0.30001816: resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} - capture-exit@2.0.0: - resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} - engines: {node: 6.* || 8.* || >= 10.*} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -10506,10 +10409,6 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} - cli-cursor@2.1.0: - resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} - engines: {node: '>=4'} - cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -10526,9 +10425,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - cli-width@2.2.1: - resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} - cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -10716,10 +10612,6 @@ packages: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} - connect@3.7.0: - resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} - engines: {node: '>= 0.10.0'} - consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} @@ -10733,16 +10625,12 @@ packages: console-table-printer@2.14.6: resolution: {integrity: sha512-MCBl5HNVaFuuHW6FGbL/4fB7N/ormCy+tQ+sxTrF6QtSbSNETvPuOVbkJBhzDgYhvjWGrTma4eYJa37ZuoQsPw==} - console-ui@3.1.2: - resolution: {integrity: sha512-+5j3R4wZJcEYZeXk30whc4ZU/+fWW9JMTNntVuMYpjZJ9n26Cxr0tUBXco1NRjVZRpRVvZ4DDKKKIHNYeUG9Dw==} - engines: {node: 6.* || 8.* || >= 10.*} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + content-disposition@2.0.1: + resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==} engines: {node: '>=18'} content-tag@4.2.0: @@ -10752,8 +10640,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@1.7.0: @@ -11192,10 +11080,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-file@1.0.0: - resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} - engines: {node: '>=0.10.0'} - detect-indent@6.0.0: resolution: {integrity: sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==} engines: {node: '>=8'} @@ -11378,6 +11262,10 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + editions@1.3.4: + resolution: {integrity: sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==} + engines: {node: '>=0.8'} + editions@2.3.1: resolution: {integrity: sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==} engines: {node: '>=0.8'} @@ -11417,8 +11305,8 @@ packages: resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} engines: {node: 6.* || 8.* || >= 10.*} - ember-cli-babel@8.3.1: - resolution: {integrity: sha512-Pxm5JP0jQ6fCBlXuh1BFmhrg2/5YXjhf16JI/n8ReOR6Nl+fEbudMpdO69LlqZRsMmTgdjCRmfSxMh26Wsw/rw==} + ember-cli-babel@8.3.2: + resolution: {integrity: sha512-Thvqx+jwGYhimYbkkL0gwhwMlhghTIR3uQ9R0yNaTjBIttZIfSWJ/2W40kaJlwhnsElXq7zbFjZcff22p6nzCg==} engines: {node: 16.* || 18.* || >= 20} peerDependencies: '@babel/core': ^7.12.0 @@ -11503,10 +11391,6 @@ packages: resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==} engines: {node: '>=10.13.0'} - enquirer@2.3.6: - resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} - engines: {node: '>=8.6'} - ensure-posix-path@1.1.1: resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} @@ -12091,13 +11975,6 @@ packages: '@sinclair/typebox': optional: true - exec-sh@0.3.6: - resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} - - execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} - execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -12110,10 +11987,6 @@ packages: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} - expand-tilde@2.0.2: - resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} - engines: {node: '>=0.10.0'} - expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -12124,12 +11997,12 @@ packages: peerDependencies: express: '>= 4.11' - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + express@4.22.3: + resolution: {integrity: sha512-Bdcs4+3qlpVlx2NRn6fgX2Ue2/gGRaPeawebgclM0ERSCqDpA+owF1fdPwjJUTAJWMTuAaxjDf+hzb0/4eKvvw==} engines: {node: '>= 0.10.0'} - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + express@5.3.0: + resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==} engines: {node: '>= 18'} exsolve@1.2.0: @@ -12211,11 +12084,14 @@ packages: fast-text-encoding@1.0.3: resolution: {integrity: sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==} - fast-uri@3.1.8: - resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fast-uri@3.1.9: + resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==} - fast-uri@4.2.1: - resolution: {integrity: sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==} + fast-uri@4.2.3: + resolution: {integrity: sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg==} + + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -12241,9 +12117,6 @@ packages: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -12266,10 +12139,6 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - figures@2.0.0: - resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} - engines: {node: '>=4'} - figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -12294,10 +12163,6 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} - engines: {node: '>= 0.8'} - finalhandler@1.3.2: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} @@ -12313,9 +12178,6 @@ packages: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} - find-index@1.1.1: - resolution: {integrity: sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw==} - find-my-way@9.7.0: resolution: {integrity: sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==} engines: {node: '>=20'} @@ -12339,18 +12201,10 @@ packages: find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} - findup-sync@5.0.0: - resolution: {integrity: sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==} - engines: {node: '>= 10.13.0'} - flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -12365,8 +12219,8 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + follow-redirects@1.16.2: + resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -12542,10 +12396,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -12626,14 +12476,6 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - global-modules@1.0.0: - resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} - engines: {node: '>=0.10.0'} - - global-prefix@1.0.2: - resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} - engines: {node: '>=0.10.0'} - globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -12840,10 +12682,6 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - homedir-polyfill@1.0.3: - resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} - engines: {node: '>=0.10.0'} - hono@4.13.11: resolution: {integrity: sha512-/SMX/RQNJn7oNmFwH6DtwDqcrzZU2otm1FD6OJe2cWF6cfy/F8hq0XVBv7xW3FTJshRQwuBnXHDq2kNsdZnshg==} engines: {node: '>=16.9.0'} @@ -12984,10 +12822,6 @@ packages: httpxy@0.5.1: resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==} - human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -13038,12 +12872,12 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.14: - resolution: {integrity: sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==} + ignore@7.0.12: + resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.14: + resolution: {integrity: sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==} engines: {node: '>= 4'} image-meta@0.3.1: @@ -13052,6 +12886,7 @@ packages: image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} + deprecated: Please migrate to image-size@2.x hasBin: true immutable@4.3.9: @@ -13110,10 +12945,6 @@ packages: injection-js@2.4.0: resolution: {integrity: sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==} - inquirer@6.5.2: - resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} - engines: {node: '>=6.0.0'} - inquirer@8.2.4: resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} engines: {node: '>=12.0.0'} @@ -13141,8 +12972,8 @@ packages: resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==} engines: {node: '>=20.0.0'} - ip-address@10.7.2: - resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==} + ip-address@10.7.6: + resolution: {integrity: sha512-00LYkM6byILa9eQIx6qoaXCM40xTLd56AZ4vyaPW3YTyb49VZYPo5xsiAq4tuQ54GH4R7gVdYScJ1B7B77PuTg==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -13228,10 +13059,6 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} - is-fullwidth-code-point@2.0.0: - resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} - engines: {node: '>=4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -13474,6 +13301,10 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + istextorbinary@2.1.0: + resolution: {integrity: sha512-kT1g2zxZ5Tdabtpp9VSdOzW9lb6LXImyWbzbQeTxoRtHhurC9Ej9Wckngr2+uepPL09ky/mJHmN9jeJPML5t6A==} + engines: {node: '>=0.12'} + istextorbinary@2.6.0: resolution: {integrity: sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA==} engines: {node: '>=0.12'} @@ -13556,15 +13387,6 @@ packages: canvas: optional: true - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} - peerDependencies: - canvas: ^3.0.0 - peerDependenciesMeta: - canvas: - optional: true - jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -13630,9 +13452,6 @@ packages: jsonc-parser@3.1.0: resolution: {integrity: sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==} - jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -14005,9 +13824,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.omit@4.18.0: - resolution: {integrity: sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==} - lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -14020,10 +13836,6 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@2.2.0: - resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} - engines: {node: '>=4'} - log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -14145,9 +13957,6 @@ packages: resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - makeerror@1.0.11: - resolution: {integrity: sha512-M/XvMZ6oK4edXjvg/ZYyzByg8kjpVrF/m0x3wbhOlzJfsQgFkqP1rJnLnJExOcslmLSSeLiN6NmF+cBoKJHGTg==} - map-age-cleaner@0.1.3: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} @@ -14404,10 +14213,6 @@ packages: engines: {node: '>=16'} hasBin: true - mimic-fn@1.2.0: - resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} - engines: {node: '>=4'} - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -14736,9 +14541,6 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - mute-stream@0.0.7: - resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} - mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -14761,8 +14563,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.9: - resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==} + nanoid@5.1.16: + resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} engines: {node: ^18 || >=20} hasBin: true @@ -14982,9 +14784,6 @@ packages: engines: {node: ^12.22 || ^14.13 || >=16} hasBin: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} @@ -15120,8 +14919,8 @@ packages: nwsapi@2.2.24: resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} - nx@22.7.10: - resolution: {integrity: sha512-T22WdfbkRi9cLH8a+FkKsNrt540DpNkzkFv1StSsphfu1povyqoVQw9Rpl/poNvnAICNKPwZcpGsy/QXMYkECw==} + nx@24.0.3: + resolution: {integrity: sha512-j3Q2ROXntVB95FQVzbIyvYVRCa2vBDBITt9N9Z/u/T7S4Um5OqfuA1os2RHaYT18kKJ7qE52rLG533T5X5U48g==} hasBin: true peerDependencies: '@swc-node/register': ^1.11.1 @@ -15207,10 +15006,6 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} - on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -15225,10 +15020,6 @@ packages: one-time@1.0.0: resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} - onetime@2.0.1: - resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} - engines: {node: '>=4'} - onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -15327,14 +15118,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@3.4.0: - resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} - engines: {node: '>=6'} - - ora@5.3.0: - resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} - engines: {node: '>=10'} - ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -15544,10 +15327,6 @@ packages: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} - parse-passwd@1.0.0: - resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} - engines: {node: '>=0.10.0'} - parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} @@ -15629,8 +15408,8 @@ packages: resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==} engines: {node: 18 || 20 || >=22} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} path-to-regexp@1.9.0: resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} @@ -16164,12 +15943,12 @@ packages: peerDependencies: postcss: ^8.2 - postcss-selector-parser@6.1.4: - resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} + postcss-selector-parser@6.1.6: + resolution: {integrity: sha512-BK87/Qgti3DIcgS2PaCVoteFIkjIFvCJBizZDIVSjdlFNg4Jv+SRqbN2JONb/qQUH4ZFqfXYF+izg15W2HVRvA==} engines: {node: '>=4'} - postcss-selector-parser@7.1.5: - resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} + postcss-selector-parser@7.2.0: + resolution: {integrity: sha512-Khh/tERVBn0I1499dvBSHY0pWJloMJWUxDsOBM0CCDPAcVZ10Jl2L1qy1f19uEX22spa8Mv7DCdEQJc0cGwBhQ==} engines: {node: '>=4'} postcss-svgo@7.1.3: @@ -16203,8 +15982,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.31: - resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} + postcss@8.5.32: + resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -16256,11 +16035,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - premove@4.0.0: - resolution: {integrity: sha512-zim/Hr4+FVdCIM7zL9b9Z0Wfd5Ya3mnKtiuDv7L5lzYzanSq6cOcVJ7EFcgK4I0pt28l8H0jX/x3nyog380XgQ==} - engines: {node: '>=6'} - hasBin: true - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -16304,8 +16078,8 @@ packages: typescript: optional: true - prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} private@0.1.8: @@ -16400,9 +16174,6 @@ packages: pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -16410,12 +16181,12 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + qs@6.16.2: + resolution: {integrity: sha512-VYdnB5S7/0IeXzBacUtwNAaM6d7Y/PgYnNC7bJCI6meqA/OgnsQbZMtVW8Dt2D1PHgbHGtYmhSoEIgvt7xZ2ZA==} engines: {node: '>=0.6'} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.17.1: + resolution: {integrity: sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -16644,9 +16415,6 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regenerator-runtime@0.13.10: - resolution: {integrity: sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==} - regenerator-runtime@0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} @@ -16749,10 +16517,6 @@ packages: resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==} engines: {node: '>=18'} - resolve-dir@1.0.1: - resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} - engines: {node: '>=0.10.0'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -16772,10 +16536,6 @@ packages: resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} engines: {node: '>= 12'} - resolve-path@1.4.0: - resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} - engines: {node: '>= 0.8'} - resolve-pathname@2.2.0: resolution: {integrity: sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==} @@ -16799,10 +16559,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - restore-cursor@2.0.0: - resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} - engines: {node: '>=4'} - restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -16856,11 +16612,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rimraf@6.1.3: - resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} - engines: {node: 20 || >=22} - hasBin: true - rolldown@1.2.16: resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -17004,11 +16755,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sane@5.0.1: - resolution: {integrity: sha512-9/0CYoRz0MKKf04OMCO3Qk3RQl1PAwWAhPSQSym4ULiLpTZnrY1JoZU0IEikHu8kdk2HvKT/VwQMq/xFZ8kh1Q==} - engines: {node: 10.* || >= 12.*} - hasBin: true - saslprep@1.0.3: resolution: {integrity: sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==} engines: {node: '>=6'} @@ -17115,11 +16861,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -17133,13 +16874,8 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - sentry@0.44.0: - resolution: {integrity: sha512-z9hagaB24WTZLKOuIdoi+XIJb2Db1k/TxPSmASvBD86H5/tqURxguLWecnraqWtDuf7ByEkUkMP7JP/mq8v89w==} - engines: {node: '>=20.0'} - hasBin: true - - sentry@0.45.0: - resolution: {integrity: sha512-kgmEbLlC22lL7uneeUm9Sjz65rp5cvwYOMTxXbZCmBum4sbVre/lLT3zTV3kwXHJ9/BM8ZS0/GhZe9Cl1t6WyA==} + sentry@0.49.0: + resolution: {integrity: sha512-9tjS0jTx5FQzhwAGhpcnwvqE0MlQiwJtunsIUNqGkw6E9CBOMbijlamIDUqvZTDtJAveVwa9L5ziP44bOJUzcQ==} engines: {node: '>=20.0'} hasBin: true @@ -17327,8 +17063,8 @@ packages: smob@1.4.1: resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==} - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} engines: {node: '>= 18'} sockjs@0.3.24: @@ -17545,10 +17281,6 @@ packages: resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} engines: {node: '>=0.10.0'} - string-width@2.1.1: - resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} - engines: {node: '>=4'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -17593,14 +17325,6 @@ packages: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} - strip-ansi@4.0.0: - resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} - engines: {node: '>=4'} - - strip-ansi@5.2.0: - resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} - engines: {node: '>=6'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -17882,9 +17606,6 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} - through2@3.0.2: - resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -17945,17 +17666,10 @@ packages: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} - tmp@0.1.0: - resolution: {integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==} - engines: {node: '>=6'} - tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-buffer@1.2.2: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} @@ -18021,10 +17735,6 @@ packages: tree-sync@1.4.0: resolution: {integrity: sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ==} - tree-sync@2.1.0: - resolution: {integrity: sha512-OLWW+Nd99NOM53aZ8ilT/YpEiOo6mXD3F4/wLbARqybSZ3Jb8IxHK5UGVbZaae0wtXAyQshVV+SeqVBik+Fbmw==} - engines: {node: '>=8'} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -18806,8 +18516,8 @@ packages: terser: optional: true - vite@6.4.3: - resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + vite@6.4.5: + resolution: {integrity: sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -18846,8 +18556,8 @@ packages: yaml: optional: true - vite@7.3.6: - resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + vite@7.3.7: + resolution: {integrity: sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -18886,8 +18596,8 @@ packages: yaml: optional: true - vite@8.3.5: - resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==} + vite@8.3.6: + resolution: {integrity: sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -18945,20 +18655,20 @@ packages: vite: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + vitest@4.1.12: + resolution: {integrity: sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@vitest/browser-playwright': 4.1.12 + '@vitest/browser-preview': 4.1.12 + '@vitest/browser-webdriverio': ^4.1.11 + '@vitest/coverage-istanbul': 4.1.12 + '@vitest/coverage-v8': 4.1.12 + '@vitest/ui': 4.1.12 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -19097,16 +18807,9 @@ packages: resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} engines: {node: '>=6.0.0'} - walker@1.0.7: - resolution: {integrity: sha512-cF4je9Fgt6sj1PKfuFt9jpQPeHosM+Ryma/hfY9U7uXGKM7pJCsF0v2r55o+Il54+i77SyYWetB4tD1dEygRkw==} - warning@3.0.0: resolution: {integrity: sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==} - watch-detector@1.0.2: - resolution: {integrity: sha512-MrJK9z7kD5Gl3jHBnnBVHvr1saVGAfmkyyrvuNzV/oe0Gr1nwZTy5VSA0Gw2j2Or0Mu8HcjUa44qlBvC2Ofnpg==} - engines: {node: '>= 8'} - watchpack@2.6.3: resolution: {integrity: sha512-s3NZy0QpTY9qGqzc0wtz4qI28z3+5qKbbAec4MV4G9N3CfgZ9wgvQRr0cl2jh/tqkWG1NWHkmVMHZpCamev5Wg==} engines: {node: '>=10.13.0'} @@ -19260,15 +18963,16 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} engines: {node: ^16.13.0 || >=18.0.0} @@ -19378,8 +19082,8 @@ packages: utf-8-validate: optional: true - ws@8.22.1: - resolution: {integrity: sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==} + ws@8.22.2: + resolution: {integrity: sha512-/rEZ2ejown2N5gxVTHUu9bVVC7ttu0/Tgzf5hbTOixR3Fs7mbbXgn2DbdbcKgJOmpQMtrIUWivtYuVMnp/W/yA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -19653,11 +19357,11 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(chokidar@3.6.0)(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(debug@4.4.3(supports-color@10.0.0))(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(lightningcss@1.33.0)(ng-packagr@14.3.0(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(@types/node@14.18.63)(supports-color@10.0.0)(tslib@2.8.1)(typescript@4.6.4))(supports-color@10.0.0)(typescript@4.6.4)(uglify-js@3.13.3)': + '@angular-devkit/build-angular@14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(chokidar@3.6.0)(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(debug@4.4.3(supports-color@10.0.0))(html-minifier-terser@6.1.0)(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(lightningcss@1.33.0)(ng-packagr@14.3.0(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(@types/node@14.18.63)(supports-color@10.0.0)(tslib@2.8.1)(typescript@4.6.4))(supports-color@10.0.0)(typescript@4.6.4)(uglify-js@3.13.3)': dependencies: '@ampproject/remapping': 2.2.0 '@angular-devkit/architect': 0.1402.13(chokidar@3.6.0) - '@angular-devkit/build-webpack': 0.1402.13(chokidar@3.6.0)(webpack-dev-server@4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + '@angular-devkit/build-webpack': 0.1402.13(chokidar@3.6.0)(webpack-dev-server@4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) '@angular-devkit/core': 14.2.13(chokidar@3.6.0) '@angular/compiler-cli': 14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4) '@babel/core': 7.18.10(supports-color@10.0.0) @@ -19670,15 +19374,15 @@ snapshots: '@babel/runtime': 7.18.9 '@babel/template': 7.18.10 '@discoveryjs/json-ext': 0.5.7 - '@ngtools/webpack': 14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(typescript@4.6.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + '@ngtools/webpack': 14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(typescript@4.6.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) ansi-colors: 4.1.3 - babel-loader: 8.2.5(@babel/core@7.18.10(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + babel-loader: 8.2.5(@babel/core@7.18.10(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) babel-plugin-istanbul: 6.1.1(supports-color@10.0.0) browserslist: 4.29.0 cacache: 16.1.2 - copy-webpack-plugin: 11.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + copy-webpack-plugin: 11.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) critters: 0.0.16 - css-loader: 6.7.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + css-loader: 6.7.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) esbuild-wasm: 0.15.5 glob: 8.0.3 https-proxy-agent: 5.0.1(supports-color@10.0.0) @@ -19686,10 +19390,10 @@ snapshots: jsonc-parser: 3.1.0 karma-source-map-support: 1.4.0 less: 4.1.3(supports-color@10.0.0) - less-loader: 11.0.0(less@4.1.3(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) - license-webpack-plugin: 4.0.2(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + less-loader: 11.0.0(less@4.1.3(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + license-webpack-plugin: 4.0.2(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) loader-utils: 3.2.1 - mini-css-extract-plugin: 2.6.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + mini-css-extract-plugin: 2.6.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) minimatch: 5.1.9 open: 8.4.0 ora: 5.4.1 @@ -19697,28 +19401,28 @@ snapshots: piscina: 3.2.0 postcss: 8.4.31 postcss-import: 15.0.0(postcss@8.4.31) - postcss-loader: 7.0.1(postcss@8.4.31)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + postcss-loader: 7.0.1(postcss@8.4.31)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) postcss-preset-env: 7.8.0(postcss@8.4.31) regenerator-runtime: 0.13.9 resolve-url-loader: 5.0.0 rxjs: 6.6.7 sass: 1.54.4 - sass-loader: 13.0.2(sass@1.54.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + sass-loader: 13.0.2(sass@1.54.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) semver: 7.5.3 - source-map-loader: 4.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + source-map-loader: 4.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) source-map-support: 0.5.21 stylus: 0.59.0(supports-color@10.0.0) - stylus-loader: 7.0.0(stylus@0.59.0(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + stylus-loader: 7.0.0(stylus@0.59.0(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) terser: 5.14.2 text-table: 0.2.0 tree-kill: 1.2.2 tslib: 2.4.0 typescript: 4.6.4 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) - webpack-dev-middleware: 5.3.3(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) - webpack-dev-server: 4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack-dev-middleware: 5.3.3(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + webpack-dev-server: 4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) webpack-merge: 5.8.0 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) optionalDependencies: esbuild: 0.15.5 ng-packagr: 14.3.0(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(@types/node@14.18.63)(supports-color@10.0.0)(tslib@2.8.1)(typescript@4.6.4) @@ -19745,12 +19449,12 @@ snapshots: - utf-8-validate - webpack-cli - '@angular-devkit/build-webpack@0.1402.13(chokidar@3.6.0)(webpack-dev-server@4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3))': + '@angular-devkit/build-webpack@0.1402.13(chokidar@3.6.0)(webpack-dev-server@4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3))': dependencies: '@angular-devkit/architect': 0.1402.13(chokidar@3.6.0) rxjs: 6.6.7 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) - webpack-dev-server: 4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack-dev-server: 4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) transitivePeerDependencies: - chokidar @@ -19886,7 +19590,7 @@ snapshots: optionalDependencies: zod: 4.6.7 - '@apm-js-collab/code-transformer-bundler-plugins@0.7.5(esbuild@0.28.1)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': + '@apm-js-collab/code-transformer-bundler-plugins@0.7.5(esbuild@0.28.1)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': dependencies: '@apm-js-collab/code-transformer': 0.19.0 es-module-lexer: 2.1.0 @@ -19895,8 +19599,8 @@ snapshots: optionalDependencies: esbuild: 0.28.1 rollup: 4.62.3 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) '@apm-js-collab/code-transformer@0.19.0': dependencies: @@ -19956,7 +19660,7 @@ snapshots: '@apollo/utils.withrequired': 3.0.0 '@graphql-tools/schema': 10.1.3(graphql@16.12.0) async-retry: 1.3.3 - body-parser: 2.3.0(supports-color@10.0.0) + body-parser: 2.4.0(supports-color@10.0.0) content-type: 1.0.5 cors: 2.8.6 finalhandler: 2.1.1(supports-color@10.0.0) @@ -20067,7 +19771,7 @@ snapshots: '@astrojs/prism@3.1.0': dependencies: - prismjs: 1.29.0 + prismjs: 1.30.0 '@astrojs/telemetry@3.1.0(supports-color@10.0.0)': dependencies: @@ -21470,21 +21174,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.24.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0)': + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0)': dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.29.7(supports-color@10.0.0)) + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.24.7(@babel/core@8.1.0)(supports-color@10.0.0)': + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@8.1.0)(supports-color@10.0.0)': dependencies: '@babel/core': 8.1.0 '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@8.1.0) + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@8.1.0) transitivePeerDependencies: - supports-color @@ -21595,12 +21299,12 @@ snapshots: '@babel/core': 7.18.10(supports-color@10.0.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators@7.25.9(@babel/core@7.29.7(supports-color@10.0.0))': + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7(supports-color@10.0.0))': dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators@7.25.9(@babel/core@8.1.0)': + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@8.1.0)': dependencies: '@babel/core': 8.1.0 '@babel/helper-plugin-utils': 7.29.7 @@ -21610,11 +21314,6 @@ snapshots: '@babel/core': 7.18.10(supports-color@10.0.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7(supports-color@10.0.0))': - dependencies: - '@babel/core': 7.29.7(supports-color@10.0.0) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.18.10(supports-color@10.0.0))': dependencies: '@babel/core': 7.18.10(supports-color@10.0.0) @@ -22998,10 +22697,6 @@ snapshots: dependencies: core-js-pure: 3.43.0 - '@babel/runtime@7.12.18': - dependencies: - regenerator-runtime: 0.13.10 - '@babel/runtime@7.18.9': dependencies: regenerator-runtime: 0.13.9 @@ -23082,11 +22777,23 @@ snapshots: '@cfworker/json-schema@4.1.1': {} + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + '@clack/core@1.5.1': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 + sisteransi: 1.0.5 + '@clack/prompts@1.8.1': dependencies: '@clack/core': 1.5.1 @@ -23110,12 +22817,12 @@ snapshots: optionalDependencies: workerd: 1.20261023.1 - '@cloudflare/vite-plugin@1.34.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(workerd@1.20260426.1)(wrangler@4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43))': + '@cloudflare/vite-plugin@1.34.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(workerd@1.20260426.1)(wrangler@4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43))': dependencies: '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260426.1) miniflare: 4.20260426.0 unenv: 2.0.0-rc.24 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) wrangler: 4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43) ws: 8.18.0 transitivePeerDependencies: @@ -23161,11 +22868,6 @@ snapshots: '@cloudflare/workers-types@5.20260710.1': {} - '@cnakazawa/watch@1.0.4': - dependencies: - exec-sh: 0.3.6 - minimist: 1.2.8 - '@colordx/core@5.5.0': {} '@colors/colors@1.6.0': {} @@ -23196,15 +22898,15 @@ snapshots: '@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.31)': dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.4) + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.6) postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.31)': + '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.32)': dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.4) - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.6) + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 '@csstools/postcss-color-function@1.1.1(postcss@8.4.31)': dependencies: @@ -23212,10 +22914,10 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-color-function@1.1.1(postcss@8.5.31)': + '@csstools/postcss-color-function@1.1.1(postcss@8.5.32)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.31) - postcss: 8.5.31 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.4.31)': @@ -23223,9 +22925,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.5.31)': + '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-hwb-function@1.0.2(postcss@8.4.31)': @@ -23233,9 +22935,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-hwb-function@1.0.2(postcss@8.5.31)': + '@csstools/postcss-hwb-function@1.0.2(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-ic-unit@1.0.1(postcss@8.4.31)': @@ -23244,32 +22946,32 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-ic-unit@1.0.1(postcss@8.5.31)': + '@csstools/postcss-ic-unit@1.0.1(postcss@8.5.32)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.31) - postcss: 8.5.31 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.4.31)': dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.4) + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.6) postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.5.31)': + '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.5.32)': dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.4) - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.6) + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 '@csstools/postcss-nested-calc@1.0.0(postcss@8.4.31)': dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-nested-calc@1.0.0(postcss@8.5.31)': + '@csstools/postcss-nested-calc@1.0.0(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.4.31)': @@ -23277,9 +22979,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.5.31)': + '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-oklab-function@1.1.1(postcss@8.4.31)': @@ -23288,10 +22990,10 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@1.1.1(postcss@8.5.31)': + '@csstools/postcss-oklab-function@1.1.1(postcss@8.5.32)': dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.31) - postcss: 8.5.31 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.4.31)': @@ -23299,9 +23001,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.5.31)': + '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.4.31)': @@ -23309,9 +23011,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.5.31)': + '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.4.31)': @@ -23319,9 +23021,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.5.31)': + '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.4.31)': @@ -23329,22 +23031,22 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.5.31)': + '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 '@csstools/postcss-unset-value@1.0.2(postcss@8.4.31)': dependencies: postcss: 8.4.31 - '@csstools/postcss-unset-value@1.0.2(postcss@8.5.31)': + '@csstools/postcss-unset-value@1.0.2(postcss@8.5.32)': dependencies: - postcss: 8.5.31 + postcss: 8.5.32 - '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.4)': + '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.6)': dependencies: - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 '@dabh/diagnostics@2.0.8': dependencies: @@ -23366,17 +23068,17 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@dxup/nuxt@0.5.6(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(esbuild@0.28.1)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': + '@dxup/nuxt@0.5.6(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(esbuild@0.28.1)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': dependencies: '@dxup/unimport': 0.1.2 - '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) '@vue/compiler-dom': 3.5.44 chokidar: 5.0.0 knitwork: 1.3.0 magic-string: 1.1.0 pathe: 2.0.3 tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) transitivePeerDependencies: - '@farmfe/core' - '@nuxt/schema' @@ -23402,9 +23104,9 @@ snapshots: '@earendil-works/pi-telemetry': 1.4.0 typebox: 1.3.27 - '@earendil-works/pi-agent-core@1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7)': + '@earendil-works/pi-agent-core@1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7)': dependencies: - '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) typebox: 1.3.27 transitivePeerDependencies: - '@aws-sdk/credential-provider-node' @@ -23419,7 +23121,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7)': + '@earendil-works/pi-ai@1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7)': dependencies: '@anthropic-ai/sdk': 0.129.0(zod@4.6.7) '@aws-sdk/client-bedrock-runtime': 3.1127.0 @@ -23428,7 +23130,7 @@ snapshots: '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@10.0.0) https-proxy-agent: 9.1.0(supports-color@10.0.0) - openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) partial-json: 0.1.7 typebox: 1.3.27 transitivePeerDependencies: @@ -23448,10 +23150,10 @@ snapshots: dependencies: quickjs-wasi: 3.6.2 - '@earendil-works/pi-durable@1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7)': + '@earendil-works/pi-durable@1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7)': dependencies: '@earendil-works/chord': 1.4.0 - '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) '@earendil-works/pi-codemode': 1.4.0 '@earendil-works/pi-telemetry': 1.4.0 '@silvia-odwyer/photon': 0.3.3 @@ -23478,10 +23180,10 @@ snapshots: dependencies: '@edge-runtime/primitives': 6.0.0 - '@effect/vitest@4.0.0(effect@4.0.0)(vitest@4.1.11)': + '@effect/vitest@4.0.0(effect@4.0.0)(vitest@4.1.12)': dependencies: effect: 4.0.0 - vitest: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vitest: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) '@ember-data/rfc395-data@0.0.4': {} @@ -23514,59 +23216,6 @@ snapshots: '@embroider/addon-shim@1.11.0': {} - '@embroider/compat@4.1.22(@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0)': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7(supports-color@10.0.0) - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.29.7(supports-color@10.0.0)) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7(supports-color@10.0.0)) - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) - '@babel/runtime': 7.29.7 - '@babel/traverse': 7.29.8(supports-color@10.0.0) - '@embroider/core': 4.6.3(@glint/template@1.8.0)(supports-color@10.0.0) - '@embroider/macros': 1.20.6(@babel/core@7.29.7(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0) - '@types/babel__code-frame': 7.27.0 - assert-never: 1.4.0 - babel-import-util: 3.0.1 - babel-plugin-debug-macros: 2.0.0(@babel/core@7.29.7(supports-color@10.0.0)) - babel-plugin-ember-template-compilation: 3.1.0 - babel-plugin-ember-template-compilation-2: babel-plugin-ember-template-compilation@2.4.1 - babel-plugin-syntax-dynamic-import: 6.18.0 - babylon: 6.18.0 - bind-decorator: 1.0.11 - broccoli: 4.0.0(supports-color@10.0.0) - broccoli-concat: 4.2.5(supports-color@10.0.0) - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8(supports-color@10.0.0) - broccoli-merge-trees: 4.2.0(supports-color@10.0.0) - broccoli-persistent-filter: 3.1.3(supports-color@10.0.0) - broccoli-plugin: 4.0.7(supports-color@10.0.0) - broccoli-source: 3.0.1 - chalk: 4.1.2 - debug: 4.4.3(supports-color@10.0.0) - fast-sourcemap-concat: 2.1.1(supports-color@10.0.0) - fs-extra: 9.1.0 - fs-tree-diff: 2.0.1(supports-color@10.0.0) - jsdom: 26.1.0(supports-color@10.0.0) - lodash: 4.18.1 - pkg-up: 3.1.0 - resolve: 1.22.12 - resolve-package-path: 4.0.3 - resolve.exports: 2.0.3 - semver: 7.8.5 - symlink-or-copy: 1.3.1 - tree-sync: 2.1.0(supports-color@10.0.0) - typescript-memoize: 1.0.1 - walk-sync: 3.0.0 - transitivePeerDependencies: - - '@glint/template' - - bufferutil - - canvas - - supports-color - - utf-8-validate - '@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0)': dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) @@ -23608,7 +23257,7 @@ snapshots: '@embroider/shared-internals': 3.1.1(supports-color@10.0.0) assert-never: 1.4.0 babel-import-util: 3.0.1 - ember-cli-babel: 8.3.1(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) + ember-cli-babel: 8.3.2(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) find-up: 5.0.0 lodash: 4.18.1 resolve: 1.22.12 @@ -23624,7 +23273,7 @@ snapshots: '@embroider/shared-internals': 3.1.1(supports-color@10.0.0) assert-never: 1.4.0 babel-import-util: 3.0.1 - ember-cli-babel: 8.3.1(@babel/core@8.1.0)(supports-color@10.0.0) + ember-cli-babel: 8.3.2(@babel/core@8.1.0)(supports-color@10.0.0) find-up: 5.0.0 lodash: 4.18.1 resolve: 1.22.12 @@ -23658,7 +23307,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@embroider/vite@1.7.9(@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@embroider/vite@1.7.9(@embroider/core@4.6.3(@glint/template@1.8.0)(supports-color@10.0.0))(@glint/template@1.8.0)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@embroider/core': 4.6.3(@glint/template@1.8.0)(supports-color@10.0.0) @@ -23676,7 +23325,7 @@ snapshots: send: 1.2.1(supports-color@10.0.0) source-map-url: 0.4.1 terser: 5.51.2 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@glint/template' - bufferutil @@ -24049,7 +23698,7 @@ snapshots: dependencies: ajv: 8.20.0 ajv-formats: 3.0.1 - fast-uri: 3.1.8 + fast-uri: 3.1.9 '@fastify/error@4.2.0': {} @@ -24068,10 +23717,10 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.4.0 - '@flue/runtime@2.3.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(typescript@7.0.2)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7)': + '@flue/runtime@2.3.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(typescript@7.0.2)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7)': dependencies: - '@earendil-works/pi-agent-core': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) - '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + '@earendil-works/pi-agent-core': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) + '@earendil-works/pi-ai': 1.4.0(@aws-sdk/credential-provider-node@3.972.92)(@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7))(@smithy/signature-v4@5.7.5)(supports-color@10.0.0)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) '@hono/node-server': 2.1.5(hono@4.13.11) '@modelcontextprotocol/client': 2.0.0 '@valibot/to-json-schema': 1.8.0(valibot@1.5.0(typescript@7.0.2)) @@ -24190,7 +23839,7 @@ snapshots: google-auth-library: 10.9.1(supports-color@10.0.0) p-retry: 4.6.2 protobufjs: 7.6.6 - ws: 8.22.1 + ws: 8.22.2 optionalDependencies: '@modelcontextprotocol/sdk': 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) transitivePeerDependencies: @@ -24203,7 +23852,7 @@ snapshots: google-auth-library: 10.9.1(supports-color@10.0.0) p-retry: 4.6.2 protobufjs: 7.6.6 - ws: 8.22.1 + ws: 8.22.2 optionalDependencies: '@modelcontextprotocol/sdk': 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) transitivePeerDependencies: @@ -24790,22 +24439,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@langchain/anthropic@0.3.31(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(zod@4.6.7)': + '@langchain/anthropic@0.3.31(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(zod@4.6.7)': dependencies: '@anthropic-ai/sdk': 0.65.0(zod@4.6.7) - '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) fast-xml-parser: 4.5.6 transitivePeerDependencies: - zod - '@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))': + '@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))': dependencies: '@cfworker/json-schema': 4.1.1 ansi-styles: 5.2.0 camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.3.74(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) + langsmith: 0.3.74(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) mustache: 4.2.0 p-queue: 6.6.2 p-retry: 4.6.2 @@ -24818,12 +24467,12 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1)': + '@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.10.10(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + langsmith: 0.10.10(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) mustache: 4.2.0 p-queue: 6.6.2 zod: 4.6.7 @@ -24834,29 +24483,29 @@ snapshots: - openai - ws - '@langchain/langgraph-checkpoint@0.0.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))': + '@langchain/langgraph-checkpoint@0.0.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))': dependencies: - '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) uuid: 10.0.0 - '@langchain/langgraph-checkpoint@1.1.6(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))': + '@langchain/langgraph-checkpoint@1.1.6(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))': dependencies: - '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) - '@langchain/langgraph-sdk@0.0.112(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@langchain/langgraph-sdk@0.0.112(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@types/json-schema': 7.0.15 p-queue: 6.6.2 p-retry: 4.6.2 uuid: 9.0.1 optionalDependencies: - '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@langchain/langgraph-sdk@1.12.3(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@langchain/langgraph-sdk@1.12.3(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) '@langchain/protocol': 0.0.19 '@types/json-schema': 7.0.15 p-queue: 9.3.3 @@ -24865,11 +24514,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@langchain/langgraph@0.2.74(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod-to-json-schema@3.25.2(zod@4.6.7))': + '@langchain/langgraph@0.2.74(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod-to-json-schema@3.25.2(zod@4.6.7))': dependencies: - '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) - '@langchain/langgraph-checkpoint': 0.0.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))) - '@langchain/langgraph-sdk': 0.0.112(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) + '@langchain/langgraph-checkpoint': 0.0.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))) + '@langchain/langgraph-sdk': 0.0.112(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) uuid: 10.0.0 zod: 3.25.76 optionalDependencies: @@ -24878,11 +24527,11 @@ snapshots: - react - react-dom - '@langchain/langgraph@1.4.21(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.6.7)': + '@langchain/langgraph@1.4.21(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.6.7)': dependencies: - '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) - '@langchain/langgraph-checkpoint': 1.1.6(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1)) - '@langchain/langgraph-sdk': 1.12.3(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) + '@langchain/langgraph-checkpoint': 1.1.6(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2)) + '@langchain/langgraph-sdk': 1.12.3(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@langchain/protocol': 0.0.19 '@standard-schema/spec': 1.1.0 zod: 4.6.7 @@ -24890,20 +24539,20 @@ snapshots: - react - react-dom - '@langchain/openai@0.5.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)))(ws@8.22.1)': + '@langchain/openai@0.5.18(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)))(ws@8.22.2)': dependencies: - '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)) js-tiktoken: 1.0.21 - openai: 5.18.1(ws@8.22.1)(zod@3.25.76) + openai: 5.18.1(ws@8.22.2)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - ws - '@langchain/openai@1.6.2(@aws-sdk/credential-provider-node@3.972.92)(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.1)': + '@langchain/openai@1.6.2(@aws-sdk/credential-provider-node@3.972.92)(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.2)': dependencies: - '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) js-tiktoken: 1.0.21 - openai: 7.31.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7) + openai: 7.31.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7) zod: 4.6.7 transitivePeerDependencies: - '@aws-sdk/credential-provider-node' @@ -24914,12 +24563,12 @@ snapshots: '@langchain/protocol@0.0.19': {} - '@langchain/typesafe@0.0.2(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(langchain@1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.1))': + '@langchain/typesafe@0.0.2(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(langchain@1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.2))': dependencies: - '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) zod: 4.6.7 optionalDependencies: - langchain: 1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.1) + langchain: 1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.2) '@leichtgewicht/ip-codec@2.0.4': {} @@ -24955,7 +24604,7 @@ snapshots: dependencies: '@modelcontextprotocol/sdk': 1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7) '@opentelemetry/semantic-conventions': 1.43.0 - ws: 8.22.1 + ws: 8.22.2 zod: 4.6.7 zod-to-json-schema: 3.25.2(zod@4.6.7) optionalDependencies: @@ -24980,10 +24629,24 @@ snapshots: pkce-challenge: 5.0.1 zod: 4.6.7 + '@modelcontextprotocol/client@2.4.0': + dependencies: + '@modelcontextprotocol/core': 2.4.0 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + jose: 6.2.10 + pkce-challenge: 5.0.1 + zod: 4.6.7 + '@modelcontextprotocol/core@2.0.0': dependencies: zod: 4.6.7 + '@modelcontextprotocol/core@2.4.0': + dependencies: + zod: 4.6.7 + '@modelcontextprotocol/sdk@1.32.2(@cfworker/json-schema@4.1.1)(supports-color@10.0.0)(zod@4.6.7)': dependencies: '@hono/node-server': 2.1.5(hono@4.13.11) @@ -24994,8 +24657,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.1.1 - express: 5.2.1(supports-color@10.0.0) - express-rate-limit: 8.7.1(express@5.2.1(supports-color@10.0.0))(supports-color@10.0.0) + express: 5.3.0(supports-color@10.0.0) + express-rate-limit: 8.7.1(express@5.3.0(supports-color@10.0.0))(supports-color@10.0.0) hono: 4.13.11 jose: 6.2.10 json-schema-typed: 8.0.2 @@ -25008,9 +24671,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/server@2.0.0': + '@modelcontextprotocol/server@2.4.0': dependencies: - '@modelcontextprotocol/core': 2.0.0 + '@modelcontextprotocol/core': 2.4.0 zod: 4.6.7 '@mongodb-js/saslprep@1.4.6': @@ -25103,11 +24766,11 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.28': optional: true - '@ngtools/webpack@14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(typescript@4.6.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3))': + '@ngtools/webpack@14.2.13(@angular/compiler-cli@14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4))(typescript@4.6.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3))': dependencies: '@angular/compiler-cli': 14.3.0(@angular/compiler@14.3.0(@angular/core@14.3.0(rxjs@7.8.2)(zone.js@0.12.0)))(supports-color@10.0.0)(typescript@4.6.4) typescript: 4.6.4 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: @@ -25218,11 +24881,11 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@3.4.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@nuxt/devtools-kit@3.4.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: - '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) execa: 8.0.1 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@nuxt/schema' - confbox @@ -25246,11 +24909,11 @@ snapshots: pkg-types: 2.4.0 semver: 7.8.5 - '@nuxt/devtools@3.4.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': + '@nuxt/devtools@3.4.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': dependencies: - '@nuxt/devtools-kit': 3.4.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@nuxt/devtools-kit': 3.4.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@nuxt/devtools-wizard': 3.4.4 - '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) '@vue/devtools-core': 8.2.1(vue@3.5.44(typescript@7.0.2)) '@vue/devtools-kit': 8.2.1 birpc: 4.2.0 @@ -25277,11 +24940,11 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))))(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) - vite-plugin-vue-tracer: 1.5.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite-plugin-inspect: 11.4.1(@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))))(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite-plugin-vue-tracer: 1.5.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) which: 6.0.1 - ws: 8.22.1 + ws: 8.22.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -25342,7 +25005,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))': + '@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))': dependencies: consola: 3.4.2 defu: 6.1.7 @@ -25360,7 +25023,7 @@ snapshots: std-env: 4.3.0 tinyexec: 1.3.1 tinyglobby: 0.2.17 - unctx: 3.0.1(magic-string@0.30.21)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + unctx: 3.0.1(magic-string@0.30.21)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) verkit: 0.5.0 optionalDependencies: '@nuxt/schema': 3.21.11 @@ -25375,7 +25038,7 @@ snapshots: - magic-string - unplugin - '@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))': + '@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))': dependencies: consola: 3.4.2 defu: 6.1.7 @@ -25393,7 +25056,7 @@ snapshots: std-env: 4.3.0 tinyexec: 1.3.1 tinyglobby: 0.2.17 - unctx: 3.0.1(magic-string@1.1.0)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + unctx: 3.0.1(magic-string@1.1.0)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) verkit: 0.5.0 optionalDependencies: '@nuxt/schema': 3.21.11 @@ -25408,7 +25071,7 @@ snapshots: - magic-string - unplugin - '@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))': + '@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@1.1.0)(mlly@1.8.2)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))': dependencies: consola: 3.4.2 defu: 6.1.7 @@ -25426,7 +25089,7 @@ snapshots: std-env: 4.3.0 tinyexec: 1.3.1 tinyglobby: 0.2.17 - unctx: 3.0.1(magic-string@1.1.0)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + unctx: 3.0.1(magic-string@1.1.0)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) verkit: 0.5.0 optionalDependencies: '@nuxt/schema': 3.21.11 @@ -25441,7 +25104,7 @@ snapshots: - magic-string - unplugin - '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3))(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': + '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5))(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.11(magicast@0.5.5) @@ -25455,11 +25118,11 @@ snapshots: escape-string-regexp: 5.0.0 exsolve: 1.2.0 h3: 1.15.11 - impound: 1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + impound: 1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.143.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - nuxt: 3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3) + nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.143.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + nuxt: 3.21.11(724864ca76bc73825b6249c96f34efd5) ohash: 2.0.12 pathe: 2.0.3 pkg-types: 2.4.0 @@ -25515,7 +25178,7 @@ snapshots: - webpack - xml2js - '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3))(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': + '@nuxt/nitro-server@3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5))(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 3.21.11(magicast@0.5.5) @@ -25529,11 +25192,11 @@ snapshots: escape-string-regexp: 5.0.0 exsolve: 1.2.0 h3: 1.15.11 - impound: 1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + impound: 1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - nuxt: 3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3) + nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + nuxt: 3.21.11(724864ca76bc73825b6249c96f34efd5) ohash: 2.0.12 pathe: 2.0.3 pkg-types: 2.4.0 @@ -25606,15 +25269,15 @@ snapshots: rc9: 3.1.0 std-env: 4.3.0 - '@nuxt/vite-builder@3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.5)(meow@13.2.0)(nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.16)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.16)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0)': + '@nuxt/vite-builder@3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.5)(meow@13.2.0)(nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.16)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.16)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0)': dependencies: '@nuxt/kit': 3.21.11(magicast@0.5.5) '@rollup/plugin-replace': 6.0.3(rollup@4.62.3) - '@vitejs/plugin-vue': 6.0.8(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) - '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.0.0)(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) - autoprefixer: 10.5.4(postcss@8.5.31) + '@vitejs/plugin-vue': 6.0.8(vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) + '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.0.0)(vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) + autoprefixer: 10.5.4(postcss@8.5.32) consola: 3.4.2 - cssnano: 7.1.9(postcss@8.5.31) + cssnano: 7.1.9(postcss@8.5.32) defu: 6.1.7 escape-string-regexp: 5.0.0 exsolve: 1.2.0 @@ -25626,20 +25289,20 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3) + nuxt: 3.21.11(724864ca76bc73825b6249c96f34efd5) nypm: 0.6.9 ohash: 2.0.12 pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.4.0 - postcss: 8.5.31 + postcss: 8.5.32 seroval: 1.6.12 std-env: 4.3.0 ufo: 1.6.4 unenv: 2.0.0-rc.24 - vite: 7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vite-node: 5.3.0(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vite-plugin-checker: 0.14.5(meow@13.2.0)(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(typescript@7.0.2)(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite-plugin-checker: 0.14.5(meow@13.2.0)(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(typescript@7.0.2)(vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) vue: 3.5.44(typescript@7.0.2) vue-bundle-renderer: 2.3.2 optionalDependencies: @@ -25676,34 +25339,34 @@ snapshots: transitivePeerDependencies: - encoding - '@nx/nx-darwin-arm64@22.7.10': + '@nx/nx-darwin-arm64@24.0.3': optional: true - '@nx/nx-darwin-x64@22.7.10': + '@nx/nx-darwin-x64@24.0.3': optional: true - '@nx/nx-freebsd-x64@22.7.10': + '@nx/nx-freebsd-x64@24.0.3': optional: true - '@nx/nx-linux-arm-gnueabihf@22.7.10': + '@nx/nx-linux-arm-gnueabihf@24.0.3': optional: true - '@nx/nx-linux-arm64-gnu@22.7.10': + '@nx/nx-linux-arm64-gnu@24.0.3': optional: true - '@nx/nx-linux-arm64-musl@22.7.10': + '@nx/nx-linux-arm64-musl@24.0.3': optional: true - '@nx/nx-linux-x64-gnu@22.7.10': + '@nx/nx-linux-x64-gnu@24.0.3': optional: true - '@nx/nx-linux-x64-musl@22.7.10': + '@nx/nx-linux-x64-musl@24.0.3': optional: true - '@nx/nx-win32-arm64-msvc@22.7.10': + '@nx/nx-win32-arm64-msvc@24.0.3': optional: true - '@nx/nx-win32-x64-msvc@22.7.10': + '@nx/nx-win32-x64-msvc@24.0.3': optional: true '@octokit/auth-token@6.0.0': {} @@ -26309,10 +25972,10 @@ snapshots: dependencies: '@prisma/debug': 6.15.0 - '@prisma/orm-family-sql@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@prisma/orm-family-sql@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: '@prisma/orm-framework': 8.0.0-rc.17(typescript@7.0.2) - '@prisma/orm-toolchain': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@standard-schema/spec': 1.1.0 arktype: 2.2.9 pathe: 2.0.3 @@ -26336,12 +25999,12 @@ snapshots: optionalDependencies: typescript: 7.0.2 - '@prisma/orm-postgres@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@prisma/orm-postgres@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: - '@prisma/orm-family-sql': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@prisma/orm-family-sql': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@prisma/orm-framework': 8.0.0-rc.17(typescript@7.0.2) - '@prisma/orm-target-postgres': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) - '@prisma/orm-toolchain': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@prisma/orm-target-postgres': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@types/node': 26.1.2 '@types/pg': 8.20.4 arktype: 2.2.9 @@ -26356,11 +26019,11 @@ snapshots: - pg-native - vite - '@prisma/orm-target-postgres@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@prisma/orm-target-postgres@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: - '@prisma/orm-family-sql': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@prisma/orm-family-sql': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@prisma/orm-framework': 8.0.0-rc.17(typescript@7.0.2) - '@prisma/orm-toolchain': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@prisma/orm-toolchain': 8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@standard-schema/spec': 1.1.0 '@types/node': 26.1.2 '@types/pg': 8.20.4 @@ -26377,7 +26040,7 @@ snapshots: - pg-native - vite - '@prisma/orm-toolchain@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@prisma/orm-toolchain@8.0.0-rc.17(magicast@0.3.5)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: '@prisma/orm-framework': 8.0.0-rc.17(typescript@7.0.2) '@vercel/detect-agent': 1.2.7 @@ -26400,7 +26063,7 @@ snapshots: wrap-ansi: 10.0.2 optionalDependencies: typescript: 7.0.2 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - magicast @@ -26447,7 +26110,7 @@ snapshots: '@protobufjs/utf8@1.1.1': {} - '@react-router/dev@7.18.4(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(react-router@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(wrangler@4.156.0(@cloudflare/workers-types@5.20260710.1)(@types/node@20.19.43))(yaml@2.9.0)': + '@react-router/dev@7.18.4(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(react-router@7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@6.0.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(wrangler@4.156.0(@cloudflare/workers-types@5.20260710.1)(@types/node@20.19.43))(yaml@2.9.0)': dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/generator': 7.29.8 @@ -26477,7 +26140,7 @@ snapshots: semver: 7.8.5 tinyglobby: 0.2.17 valibot: 1.5.0(typescript@6.0.3) - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vite-node: 3.2.4(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) optionalDependencies: typescript: 6.0.3 @@ -26903,7 +26566,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@sentry/conventions@0.30.0': {} + '@sentry/conventions@0.32.0': {} '@sentry/node-cpu-profiler@2.4.4': dependencies: @@ -26915,22 +26578,22 @@ snapshots: detect-libc: 2.1.2 node-abi: 3.89.0 - '@sentry/rrdom@2.45.0': + '@sentry/rrdom@2.46.0': dependencies: - '@sentry/rrweb-snapshot': 2.45.0 + '@sentry/rrweb-snapshot': 2.46.0 - '@sentry/rrweb-snapshot@2.45.0': {} + '@sentry/rrweb-snapshot@2.46.0': {} - '@sentry/rrweb-types@2.45.0': + '@sentry/rrweb-types@2.46.0': dependencies: - '@sentry/rrweb-snapshot': 2.45.0 + '@sentry/rrweb-snapshot': 2.46.0 '@types/css-font-loading-module': 0.0.7 - '@sentry/rrweb@2.45.0': + '@sentry/rrweb@2.46.0': dependencies: - '@sentry/rrdom': 2.45.0 - '@sentry/rrweb-snapshot': 2.45.0 - '@sentry/rrweb-types': 2.45.0 + '@sentry/rrdom': 2.46.0 + '@sentry/rrweb-snapshot': 2.46.0 + '@sentry/rrweb-types': 2.46.0 '@types/css-font-loading-module': 0.0.7 '@xstate/fsm': 1.6.5 base64-arraybuffer: 1.0.2 @@ -26983,18 +26646,18 @@ snapshots: '@size-limit/esbuild@12.1.0(size-limit@12.1.0(jiti@2.7.0))': dependencies: esbuild: 0.28.1 - nanoid: 5.1.9 + nanoid: 5.1.16 size-limit: 12.1.0(jiti@2.7.0) '@size-limit/file@12.1.0(size-limit@12.1.0(jiti@2.7.0))': dependencies: size-limit: 12.1.0(jiti@2.7.0) - '@size-limit/webpack@12.1.0(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(size-limit@12.1.0(jiti@2.7.0))(svgo@4.1.0)(uglify-js@3.13.3)': + '@size-limit/webpack@12.1.0(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(size-limit@12.1.0(jiti@2.7.0))(svgo@4.1.0)(uglify-js@3.13.3)': dependencies: - nanoid: 5.1.9 + nanoid: 5.1.16 size-limit: 12.1.0(jiti@2.7.0) - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) transitivePeerDependencies: - '@minify-html/node' - '@napi-rs/image' @@ -27408,11 +27071,11 @@ snapshots: dependencies: solid-js: 1.9.18 - '@solidjs/start@1.3.2(e4a8c665f4a8ec10ddcce54d0b28f89d)': + '@solidjs/start@1.3.2(1649c7f604f12e5f9af57e229fd2f3f2)': dependencies: - '@tanstack/server-functions-plugin': 1.121.21(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0)) - '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0)) + '@tanstack/server-functions-plugin': 1.121.21(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0)) cookie-es: 2.0.1 defu: 6.1.7 error-stack-parser: 2.1.4 @@ -27424,8 +27087,8 @@ snapshots: source-map-js: 1.2.2 terracotta: 1.0.5(solid-js@1.9.18) tinyglobby: 0.2.17 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) - vite-plugin-solid: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) + vite-plugin-solid: 2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -27480,11 +27143,11 @@ snapshots: dependencies: acorn: 8.18.0 - '@sveltejs/kit@2.70.2(@opentelemetry/api@1.10.0)(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(svelte@4.2.8)(typescript@6.0.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@sveltejs/kit@2.70.2(@opentelemetry/api@1.10.0)(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(svelte@4.2.8)(typescript@6.0.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) - '@sveltejs/vite-plugin-svelte': 3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.18.0 cookie: 0.6.0 @@ -27496,21 +27159,21 @@ snapshots: set-cookie-parser: 3.0.1 sirv: 3.0.2 svelte: 4.2.8 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) optionalDependencies: '@opentelemetry/api': 1.10.0 typescript: 6.0.3 - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) debug: 4.4.3(supports-color@10.0.0) svelte: 4.2.8 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@1.4.0(supports-color@10.0.0)(svelte@3.59.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@1.4.0(supports-color@10.0.0)(svelte@3.59.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: debug: 4.4.3(supports-color@10.0.0) deepmerge: 4.3.1 @@ -27518,22 +27181,22 @@ snapshots: magic-string: 0.26.7 svelte: 3.59.2 svelte-hmr: 0.15.3(svelte@3.59.2) - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vitefu: 0.2.5(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vitefu: 0.2.5(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)))(supports-color@10.0.0)(svelte@4.2.8)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) debug: 4.4.3(supports-color@10.0.0) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 4.2.8 svelte-hmr: 0.16.0(svelte@4.2.8) - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vitefu: 0.2.5(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vitefu: 0.2.5(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -27541,7 +27204,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@tanstack/directive-functions-plugin@1.121.21(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@tanstack/directive-functions-plugin@1.121.21(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.7(supports-color@10.0.0) @@ -27550,7 +27213,7 @@ snapshots: '@tanstack/router-utils': 1.154.7(supports-color@10.0.0) babel-dead-code-elimination: 1.0.12(supports-color@10.0.0) tiny-invariant: 1.3.3 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -27587,7 +27250,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.121.21(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@tanstack/server-functions-plugin@1.121.21(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.7(supports-color@10.0.0) @@ -27596,7 +27259,7 @@ snapshots: '@babel/template': 7.29.7 '@babel/traverse': 7.29.8(supports-color@10.0.0) '@babel/types': 7.29.8 - '@tanstack/directive-functions-plugin': 1.121.21(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@tanstack/directive-functions-plugin': 1.121.21(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) babel-dead-code-elimination: 1.0.12(supports-color@10.0.0) tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -27760,8 +27423,6 @@ snapshots: '@types/aws-lambda@8.10.164': {} - '@types/babel__code-frame@7.27.0': {} - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.9 @@ -27930,11 +27591,6 @@ snapshots: dependencies: '@types/unist': 3.0.0 - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 20.19.43 - form-data: 4.0.6 - '@types/node-forge@1.3.11': dependencies: '@types/node': 14.18.63 @@ -28373,7 +28029,7 @@ snapshots: untun: 0.1.3 uqr: 0.1.3 - '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0))': + '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0))': dependencies: '@babel/parser': 7.29.9 acorn: 8.18.0 @@ -28384,38 +28040,38 @@ snapshots: magicast: 0.2.11 recast: 0.23.11 tslib: 2.8.1 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) - '@vinxi/server-components@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0))': + '@vinxi/server-components@0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0))': dependencies: - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0)) acorn: 8.18.0 acorn-loose: 8.4.0 acorn-typescript: 1.4.13(acorn@8.18.0) astring: 1.9.0 magicast: 0.2.11 recast: 0.23.11 - vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) + vinxi: 0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0) - '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.0.0)(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': + '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.0.0)(vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@rolldown/pluginutils': 1.0.1 '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) - vite: 7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vue: 3.5.44(typescript@7.0.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.8(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': + '@vitejs/plugin-vue@6.0.8(vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vue: 3.5.44(typescript@7.0.2) - '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': + '@vitest/coverage-v8@4.1.11(vitest@4.1.12)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.11 @@ -28427,42 +28083,46 @@ snapshots: obug: 2.1.4 std-env: 4.3.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vitest: 4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - '@vitest/expect@4.1.11': + '@vitest/expect@4.1.12': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.11(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': + '@vitest/mocker@4.1.12(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.11 + '@vitest/spy': 4.1.12 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) '@vitest/pretty-format@4.1.11': dependencies: tinyrainbow: 3.1.1 - '@vitest/runner@4.1.11': + '@vitest/pretty-format@4.1.12': dependencies: - '@vitest/utils': 4.1.11 + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.12': + dependencies: + '@vitest/utils': 4.1.12 pathe: 2.0.3 - '@vitest/snapshot@4.1.11': + '@vitest/snapshot@4.1.12': dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 + '@vitest/pretty-format': 4.1.12 + '@vitest/utils': 4.1.12 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.11': {} + '@vitest/spy@4.1.12': {} '@vitest/utils@4.1.11': dependencies: @@ -28470,6 +28130,12 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.1 + '@vitest/utils@4.1.12': + dependencies: + '@vitest/pretty-format': 4.1.12 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + '@volar/kit@2.4.28(typescript@5.8.3)': dependencies: '@volar/language-service': 2.4.28(typescript@5.8.3) @@ -28588,7 +28254,7 @@ snapshots: '@vue/shared': 3.5.44 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.31 + postcss: 8.5.32 source-map-js: 1.2.2 '@vue/compiler-ssr@3.5.44': @@ -28884,12 +28550,6 @@ snapshots: transitivePeerDependencies: - supports-color - agent-base@6.0.2(supports-color@7.2.0): - dependencies: - debug: 4.4.3(supports-color@7.2.0) - transitivePeerDependencies: - - supports-color - agent-base@6.0.2(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -28968,7 +28628,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.8 + fast-uri: 3.1.9 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -28992,20 +28652,12 @@ snapshots: ansi-colors@4.1.3: {} - ansi-escapes@3.2.0: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 ansi-html-community@0.0.8: {} - ansi-html@0.0.9: {} - - ansi-regex@3.0.1: {} - - ansi-regex@4.1.1: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -29273,6 +28925,18 @@ snapshots: - terser - typescript + async-disk-cache@1.3.5(supports-color@10.0.0): + dependencies: + debug: 2.6.9(supports-color@10.0.0) + heimdalljs: 0.2.6 + istextorbinary: 2.1.0 + mkdirp: 0.5.6 + rimraf: 2.7.1 + rsvp: 3.6.2 + username-sync: 1.0.2 + transitivePeerDependencies: + - supports-color + async-disk-cache@2.1.0(supports-color@10.0.0): dependencies: debug: 4.4.3(supports-color@10.0.0) @@ -29327,13 +28991,13 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - autoprefixer@10.5.4(postcss@8.5.31): + autoprefixer@10.5.4(postcss@8.5.32): dependencies: browserslist: 4.29.0 caniuse-lite: 1.0.30001816 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -29347,19 +29011,9 @@ snapshots: aws-ssl-profiles@1.1.2: {} - axios@1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): - dependencies: - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) - form-data: 4.0.6 - https-proxy-agent: 5.0.1(supports-color@7.2.0) - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - - supports-color - axios@1.21.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0): dependencies: - follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.0.0)) + follow-redirects: 1.16.2(debug@4.4.3(supports-color@10.0.0)) form-data: 4.0.6 https-proxy-agent: 5.0.1(supports-color@10.0.0) proxy-from-env: 2.1.0 @@ -29384,25 +29038,23 @@ snapshots: transitivePeerDependencies: - supports-color - babel-import-util@2.1.1: {} - babel-import-util@3.0.1: {} - babel-loader@10.1.1(@babel/core@7.29.7(supports-color@10.0.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + babel-loader@10.1.1(@babel/core@7.29.7(supports-color@10.0.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) find-up: 5.0.0 optionalDependencies: - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) - babel-loader@8.2.5(@babel/core@7.18.10(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + babel-loader@8.2.5(@babel/core@7.18.10(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: '@babel/core': 7.18.10(supports-color@10.0.0) find-cache-dir: 3.3.2 loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) babel-plugin-debug-macros@0.3.4(@babel/core@7.29.7(supports-color@10.0.0)): dependencies: @@ -29414,12 +29066,6 @@ snapshots: '@babel/core': 8.1.0 semver: 5.7.2 - babel-plugin-debug-macros@2.0.0(@babel/core@7.29.7(supports-color@10.0.0)): - dependencies: - '@babel/core': 7.29.7(supports-color@10.0.0) - babel-import-util: 2.1.1 - semver: 7.8.5 - babel-plugin-ember-data-packages-polyfill@0.1.2: dependencies: '@ember-data/rfc395-data': 0.0.4 @@ -29428,11 +29074,6 @@ snapshots: dependencies: ember-rfc176-data: 0.3.17 - babel-plugin-ember-template-compilation@2.4.1: - dependencies: - '@glimmer/syntax': 0.95.0 - babel-import-util: 3.0.1 - babel-plugin-ember-template-compilation@3.1.0: dependencies: '@glimmer/syntax': 0.95.0 @@ -29554,15 +29195,11 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-syntax-dynamic-import@6.18.0: {} - babel-preset-solid@1.8.17(@babel/core@7.29.7(supports-color@10.0.0)): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) babel-plugin-jsx-dom-expressions: 0.37.21(@babel/core@7.29.7(supports-color@10.0.0)) - babylon@6.18.0: {} - backburner.js@2.8.0: {} bail@2.0.2: {} @@ -29607,8 +29244,6 @@ snapshots: binaryextensions@2.3.0: {} - bind-decorator@1.0.11: {} - bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -29637,7 +29272,7 @@ snapshots: blake3-wasm@2.1.5: {} - body-parser@1.20.4(supports-color@10.0.0): + body-parser@1.20.9(supports-color@10.0.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -29647,22 +29282,22 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.2 + qs: 6.16.2 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - body-parser@2.3.0(supports-color@10.0.0): + body-parser@2.4.0(supports-color@10.0.0): dependencies: bytes: 3.1.2 - content-type: 2.0.0 + content-type: 2.1.0 debug: 4.4.3(supports-color@10.0.0) http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.17.1 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -29703,10 +29338,6 @@ snapshots: dependencies: balanced-match: 4.0.3 - brace-expansion@5.0.8: - dependencies: - balanced-match: 4.0.3 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -29739,22 +29370,6 @@ snapshots: transitivePeerDependencies: - supports-color - broccoli-concat@4.2.5(supports-color@10.0.0): - dependencies: - broccoli-debug: 0.6.5(supports-color@10.0.0) - broccoli-kitchen-sink-helpers: 0.3.1 - broccoli-plugin: 4.0.7(supports-color@10.0.0) - ensure-posix-path: 1.1.1 - fast-sourcemap-concat: 2.1.1(supports-color@10.0.0) - find-index: 1.1.1 - fs-extra: 8.1.0 - fs-tree-diff: 2.0.1(supports-color@10.0.0) - lodash.merge: 4.6.2 - lodash.omit: 4.18.0 - lodash.uniq: 4.5.0 - transitivePeerDependencies: - - supports-color - broccoli-debug@0.6.5(supports-color@10.0.0): dependencies: broccoli-plugin: 1.3.1 @@ -29807,6 +29422,24 @@ snapshots: transitivePeerDependencies: - supports-color + broccoli-persistent-filter@1.4.6(supports-color@10.0.0): + dependencies: + async-disk-cache: 1.3.5(supports-color@10.0.0) + async-promise-queue: 1.0.5(supports-color@10.0.0) + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9(supports-color@10.0.0) + hash-for-dep: 1.5.1(supports-color@10.0.0) + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10(supports-color@10.0.0) + mkdirp: 0.5.6 + promise-map-series: 0.2.3 + rimraf: 2.7.1 + rsvp: 3.6.2 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + broccoli-persistent-filter@3.1.3(supports-color@10.0.0): dependencies: async-disk-cache: 2.1.0(supports-color@10.0.0) @@ -29846,26 +29479,10 @@ snapshots: dependencies: broccoli-node-api: 1.7.0 - broccoli@4.0.0(supports-color@10.0.0): + broccoli-string-replace@0.1.2(supports-color@10.0.0): dependencies: - ansi-html: 0.0.9 - broccoli-node-info: 2.2.0 - broccoli-source: 3.0.1 - commander: 14.0.3 - connect: 3.7.0(supports-color@10.0.0) - console-ui: 3.1.2 - findup-sync: 5.0.0 - handlebars: 4.7.10 - heimdalljs: 0.2.6 - heimdalljs-logger: 0.1.10(supports-color@10.0.0) - mime-types: 3.0.2 - resolve-path: 1.4.0 - rimraf: 6.1.3 - sane: 5.0.1 - tmp: 0.2.7 - tree-sync: 2.1.0(supports-color@10.0.0) - underscore.string: 3.3.6 - watch-detector: 1.0.2(supports-color@10.0.0) + broccoli-persistent-filter: 1.4.6(supports-color@10.0.0) + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -29882,10 +29499,6 @@ snapshots: node-releases: 2.0.59 update-browserslist-db: 1.3.4(browserslist@4.29.0) - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - bson@1.1.6: {} bson@4.7.2: @@ -30103,10 +29716,6 @@ snapshots: caniuse-lite@1.0.30001816: {} - capture-exit@2.0.0: - dependencies: - rsvp: 4.8.5 - ccount@2.0.1: {} chai@6.2.2: {} @@ -30184,10 +29793,6 @@ snapshots: cli-boxes@3.0.0: {} - cli-cursor@2.1.0: - dependencies: - restore-cursor: 2.0.0 - cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -30200,8 +29805,6 @@ snapshots: cli-spinners@2.9.2: {} - cli-width@2.2.1: {} - cli-width@3.0.0: {} client-only@0.0.1: {} @@ -30319,7 +29922,8 @@ snapshots: commander@12.1.0: {} - commander@14.0.3: {} + commander@14.0.3: + optional: true commander@2.20.3: {} @@ -30374,15 +29978,6 @@ snapshots: connect-history-api-fallback@2.0.0: {} - connect@3.7.0(supports-color@10.0.0): - dependencies: - debug: 2.6.9(supports-color@10.0.0) - finalhandler: 1.1.2(supports-color@10.0.0) - parseurl: 1.3.3 - utils-merge: 1.0.1 - transitivePeerDependencies: - - supports-color - consola@2.15.3: {} consola@3.4.2: {} @@ -30393,25 +29988,17 @@ snapshots: dependencies: simple-wcswidth: 1.1.2 - console-ui@3.1.2: - dependencies: - chalk: 2.4.2 - inquirer: 6.5.2 - json-stable-stringify: 1.0.1 - ora: 3.4.0 - through2: 3.0.2 - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 - content-disposition@1.0.1: {} + content-disposition@2.0.1: {} content-tag@4.2.0: {} content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@1.7.0: dependencies: @@ -30444,15 +30031,15 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@11.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + copy-webpack-plugin@11.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 globby: 13.2.2 normalize-path: 3.0.0 - schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) serialize-javascript: 6.0.2 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) core-js-compat@3.50.0: dependencies: @@ -30528,28 +30115,28 @@ snapshots: css-blank-pseudo@3.0.3(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - css-blank-pseudo@3.0.3(postcss@8.5.31): + css-blank-pseudo@3.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - css-declaration-sorter@7.2.0(postcss@8.5.31): + css-declaration-sorter@7.2.0(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 css-has-pseudo@3.0.4(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - css-has-pseudo@3.0.4(postcss@8.5.31): + css-has-pseudo@3.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - css-loader@6.7.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + css-loader@6.7.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: icss-utils: 5.1.0(postcss@8.4.31) postcss: 8.4.31 @@ -30559,15 +30146,15 @@ snapshots: postcss-modules-values: 4.0.0(postcss@8.4.31) postcss-value-parser: 4.2.0 semver: 7.8.5 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) css-prefers-color-scheme@6.0.3(postcss@8.4.31): dependencies: postcss: 8.4.31 - css-prefers-color-scheme@6.0.3(postcss@8.5.31): + css-prefers-color-scheme@6.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 css-select@4.3.0: dependencies: @@ -30610,49 +30197,49 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.17(postcss@8.5.31): + cssnano-preset-default@7.0.17(postcss@8.5.32): dependencies: browserslist: 4.29.0 - css-declaration-sorter: 7.2.0(postcss@8.5.31) - cssnano-utils: 5.0.3(postcss@8.5.31) - postcss: 8.5.31 - postcss-calc: 10.1.1(postcss@8.5.31) - postcss-colormin: 7.0.10(postcss@8.5.31) - postcss-convert-values: 7.0.12(postcss@8.5.31) - postcss-discard-comments: 7.0.8(postcss@8.5.31) - postcss-discard-duplicates: 7.0.4(postcss@8.5.31) - postcss-discard-empty: 7.0.3(postcss@8.5.31) - postcss-discard-overridden: 7.0.3(postcss@8.5.31) - postcss-merge-longhand: 7.0.7(postcss@8.5.31) - postcss-merge-rules: 7.0.11(postcss@8.5.31) - postcss-minify-font-values: 7.0.3(postcss@8.5.31) - postcss-minify-gradients: 7.0.5(postcss@8.5.31) - postcss-minify-params: 7.0.9(postcss@8.5.31) - postcss-minify-selectors: 7.1.2(postcss@8.5.31) - postcss-normalize-charset: 7.0.3(postcss@8.5.31) - postcss-normalize-display-values: 7.0.3(postcss@8.5.31) - postcss-normalize-positions: 7.0.4(postcss@8.5.31) - postcss-normalize-repeat-style: 7.0.4(postcss@8.5.31) - postcss-normalize-string: 7.0.3(postcss@8.5.31) - postcss-normalize-timing-functions: 7.0.3(postcss@8.5.31) - postcss-normalize-unicode: 7.0.9(postcss@8.5.31) - postcss-normalize-url: 7.0.3(postcss@8.5.31) - postcss-normalize-whitespace: 7.0.3(postcss@8.5.31) - postcss-ordered-values: 7.0.4(postcss@8.5.31) - postcss-reduce-initial: 7.0.9(postcss@8.5.31) - postcss-reduce-transforms: 7.0.3(postcss@8.5.31) - postcss-svgo: 7.1.3(postcss@8.5.31) - postcss-unique-selectors: 7.0.7(postcss@8.5.31) - - cssnano-utils@5.0.3(postcss@8.5.31): - dependencies: - postcss: 8.5.31 - - cssnano@7.1.9(postcss@8.5.31): - dependencies: - cssnano-preset-default: 7.0.17(postcss@8.5.31) + css-declaration-sorter: 7.2.0(postcss@8.5.32) + cssnano-utils: 5.0.3(postcss@8.5.32) + postcss: 8.5.32 + postcss-calc: 10.1.1(postcss@8.5.32) + postcss-colormin: 7.0.10(postcss@8.5.32) + postcss-convert-values: 7.0.12(postcss@8.5.32) + postcss-discard-comments: 7.0.8(postcss@8.5.32) + postcss-discard-duplicates: 7.0.4(postcss@8.5.32) + postcss-discard-empty: 7.0.3(postcss@8.5.32) + postcss-discard-overridden: 7.0.3(postcss@8.5.32) + postcss-merge-longhand: 7.0.7(postcss@8.5.32) + postcss-merge-rules: 7.0.11(postcss@8.5.32) + postcss-minify-font-values: 7.0.3(postcss@8.5.32) + postcss-minify-gradients: 7.0.5(postcss@8.5.32) + postcss-minify-params: 7.0.9(postcss@8.5.32) + postcss-minify-selectors: 7.1.2(postcss@8.5.32) + postcss-normalize-charset: 7.0.3(postcss@8.5.32) + postcss-normalize-display-values: 7.0.3(postcss@8.5.32) + postcss-normalize-positions: 7.0.4(postcss@8.5.32) + postcss-normalize-repeat-style: 7.0.4(postcss@8.5.32) + postcss-normalize-string: 7.0.3(postcss@8.5.32) + postcss-normalize-timing-functions: 7.0.3(postcss@8.5.32) + postcss-normalize-unicode: 7.0.9(postcss@8.5.32) + postcss-normalize-url: 7.0.3(postcss@8.5.32) + postcss-normalize-whitespace: 7.0.3(postcss@8.5.32) + postcss-ordered-values: 7.0.4(postcss@8.5.32) + postcss-reduce-initial: 7.0.9(postcss@8.5.32) + postcss-reduce-transforms: 7.0.3(postcss@8.5.32) + postcss-svgo: 7.1.3(postcss@8.5.32) + postcss-unique-selectors: 7.0.7(postcss@8.5.32) + + cssnano-utils@5.0.3(postcss@8.5.32): + dependencies: + postcss: 8.5.32 + + cssnano@7.1.9(postcss@8.5.32): + dependencies: + cssnano-preset-default: 7.0.17(postcss@8.5.32) lilconfig: 3.1.3 - postcss: 8.5.31 + postcss: 8.5.32 csso@5.0.5: dependencies: @@ -30874,8 +30461,6 @@ snapshots: destroy@1.2.0: {} - detect-file@1.0.0: {} - detect-indent@6.0.0: {} detect-libc@2.1.2: {} @@ -30898,11 +30483,11 @@ snapshots: dependencies: node-source-walk: 7.0.1 - detective-postcss@7.0.1(postcss@8.5.31): + detective-postcss@7.0.1(postcss@8.5.32): dependencies: is-url: 1.2.4 - postcss: 8.5.31 - postcss-values-parser: 6.0.2(postcss@8.5.31) + postcss: 8.5.32 + postcss-values-parser: 6.0.2(postcss@8.5.32) detective-sass@6.0.1: dependencies: @@ -31064,6 +30649,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + editions@1.3.4: {} + editions@2.3.1: dependencies: errlop: 2.2.0 @@ -31094,11 +30681,11 @@ snapshots: ember-cli-babel-plugin-helpers@1.1.1: {} - ember-cli-babel@8.3.1(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0): + ember-cli-babel@8.3.2(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/helper-compilation-targets': 7.29.7 - '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) @@ -31107,7 +30694,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) - '@babel/runtime': 7.12.18 + '@babel/runtime': 7.29.7 amd-name-resolver: 1.3.1 babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.7(supports-color@10.0.0)) babel-plugin-ember-data-packages-polyfill: 0.1.2 @@ -31117,6 +30704,7 @@ snapshots: broccoli-debug: 0.6.5(supports-color@10.0.0) broccoli-funnel: 3.0.8(supports-color@10.0.0) broccoli-source: 3.0.1 + broccoli-string-replace: 0.1.2(supports-color@10.0.0) calculate-cache-key-for-tree: 2.0.0 clone: 2.1.2 ember-cli-babel-plugin-helpers: 1.1.1 @@ -31127,11 +30715,11 @@ snapshots: transitivePeerDependencies: - supports-color - ember-cli-babel@8.3.1(@babel/core@8.1.0)(supports-color@10.0.0): + ember-cli-babel@8.3.2(@babel/core@8.1.0)(supports-color@10.0.0): dependencies: '@babel/core': 8.1.0 '@babel/helper-compilation-targets': 7.29.7 - '@babel/plugin-proposal-decorators': 7.24.7(@babel/core@8.1.0)(supports-color@10.0.0) + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) @@ -31140,7 +30728,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) '@babel/preset-env': 7.29.7(@babel/core@8.1.0)(supports-color@10.0.0) - '@babel/runtime': 7.12.18 + '@babel/runtime': 7.29.7 amd-name-resolver: 1.3.1 babel-plugin-debug-macros: 0.3.4(@babel/core@8.1.0) babel-plugin-ember-data-packages-polyfill: 0.1.2 @@ -31150,6 +30738,7 @@ snapshots: broccoli-debug: 0.6.5(supports-color@10.0.0) broccoli-funnel: 3.0.8(supports-color@10.0.0) broccoli-source: 3.0.1 + broccoli-string-replace: 0.1.2(supports-color@10.0.0) calculate-cache-key-for-tree: 2.0.0 clone: 2.1.2 ember-cli-babel-plugin-helpers: 1.1.1 @@ -31210,7 +30799,7 @@ snapshots: broccoli-funnel: 3.0.8(supports-color@10.0.0) broccoli-merge-trees: 4.2.0(supports-color@10.0.0) chalk: 4.1.2 - ember-cli-babel: 8.3.1(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) + ember-cli-babel: 8.3.2(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) ember-cli-get-component-path-option: 1.0.0 ember-cli-is-package-missing: 1.0.0 ember-cli-normalize-entity-name: 1.0.0(supports-color@10.0.0) @@ -31263,10 +30852,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 - enquirer@2.3.6: - dependencies: - ansi-colors: 4.1.3 - ensure-posix-path@1.1.1: {} ent@2.2.0: {} @@ -31927,20 +31512,6 @@ snapshots: exact-mirror@0.2.7: {} - exec-sh@0.3.6: {} - - execa@4.1.0: - dependencies: - cross-spawn: 7.0.6 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -31967,25 +31538,21 @@ snapshots: exit-hook@2.2.1: {} - expand-tilde@2.0.2: - dependencies: - homedir-polyfill: 1.0.3 - expect-type@1.4.0: {} - express-rate-limit@8.7.1(express@5.2.1(supports-color@10.0.0))(supports-color@10.0.0): + express-rate-limit@8.7.1(express@5.3.0(supports-color@10.0.0))(supports-color@10.0.0): dependencies: debug: 4.4.3(supports-color@10.0.0) - express: 5.2.1(supports-color@10.0.0) - ip-address: 10.7.2 + express: 5.3.0(supports-color@10.0.0) + ip-address: 10.7.6 transitivePeerDependencies: - supports-color - express@4.22.1(supports-color@10.0.0): + express@4.22.3(supports-color@10.0.0): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4(supports-color@10.0.0) + body-parser: 1.20.9(supports-color@10.0.0) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -32002,9 +31569,9 @@ snapshots: methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.12 + path-to-regexp: 0.1.13 proxy-addr: 2.0.8 - qs: 6.14.2 + qs: 6.16.2 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.2(supports-color@10.0.0) @@ -32017,12 +31584,12 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1(supports-color@10.0.0): + express@5.3.0(supports-color@10.0.0): dependencies: accepts: 2.0.0 - body-parser: 2.3.0(supports-color@10.0.0) - content-disposition: 1.0.1 - content-type: 1.0.5 + body-parser: 2.4.0(supports-color@10.0.0) + content-disposition: 2.0.1 + content-type: 2.1.0 cookie: 0.7.2 cookie-signature: 1.2.2 debug: 4.4.3(supports-color@10.0.0) @@ -32039,7 +31606,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.8 - qs: 6.15.3 + qs: 6.17.1 range-parser: 1.2.1 router: 2.2.0(supports-color@10.0.0) send: 1.2.1(supports-color@10.0.0) @@ -32100,7 +31667,7 @@ snapshots: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.20.0 ajv-formats: 3.0.1 - fast-uri: 3.1.8 + fast-uri: 3.1.9 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -32109,7 +31676,7 @@ snapshots: '@fastify/merge-json-schemas': 0.2.1 ajv: 8.20.0 ajv-formats: 3.0.1 - fast-uri: 4.2.1 + fast-uri: 4.2.3 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -32147,9 +31714,13 @@ snapshots: fast-text-encoding@1.0.3: {} - fast-uri@3.1.8: {} + fast-uri@3.1.9: {} - fast-uri@4.2.1: {} + fast-uri@4.2.3: {} + + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 fast-wrap-ansi@0.2.2: dependencies: @@ -32197,10 +31768,6 @@ snapshots: dependencies: websocket-driver: 0.7.5 - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -32216,10 +31783,6 @@ snapshots: fflate@0.8.3: {} - figures@2.0.0: - dependencies: - escape-string-regexp: 1.0.5 - figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -32257,18 +31820,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.1.2(supports-color@10.0.0): - dependencies: - debug: 2.6.9(supports-color@10.0.0) - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.3.0 - parseurl: 1.3.3 - statuses: 1.5.0 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - finalhandler@1.3.2(supports-color@10.0.0): dependencies: debug: 2.6.9(supports-color@10.0.0) @@ -32303,8 +31854,6 @@ snapshots: make-dir: 3.1.0 pkg-dir: 4.2.0 - find-index@1.1.1: {} - find-my-way@9.7.0: dependencies: fast-deep-equal: 3.1.3 @@ -32332,20 +31881,11 @@ snapshots: micromatch: 4.0.8 pkg-dir: 4.2.0 - findup-sync@5.0.0: - dependencies: - detect-file: 1.0.0 - is-glob: 4.0.3 - micromatch: 4.0.8 - resolve-dir: 1.0.1 - flat-cache@3.0.4: dependencies: flatted: 3.4.2 rimraf: 3.0.2 - flat@5.0.2: {} - flatted@3.4.2: {} flattie@1.1.1: {} @@ -32354,14 +31894,10 @@ snapshots: fn.name@1.1.0: {} - follow-redirects@1.16.0(debug@4.4.3(supports-color@10.0.0)): + follow-redirects@1.16.2(debug@4.4.3(supports-color@10.0.0)): optionalDependencies: debug: 4.4.3(supports-color@10.0.0) - follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): - optionalDependencies: - debug: 4.4.3(supports-color@7.2.0) - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -32584,10 +32120,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.0 - get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -32692,20 +32224,6 @@ snapshots: dependencies: ini: 4.1.1 - global-modules@1.0.0: - dependencies: - global-prefix: 1.0.2 - is-windows: 1.0.2 - resolve-dir: 1.0.1 - - global-prefix@1.0.2: - dependencies: - expand-tilde: 2.0.2 - homedir-polyfill: 1.0.3 - ini: 1.3.8 - is-windows: 1.0.2 - which: 1.3.1 - globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -33033,10 +32551,6 @@ snapshots: dependencies: react-is: 16.13.1 - homedir-polyfill@1.0.3: - dependencies: - parse-passwd: 1.0.0 - hono@4.13.11: {} hono@5.0.0-rc.1: {} @@ -33086,7 +32600,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)): + html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -33094,10 +32608,10 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3) optional: true - html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -33105,7 +32619,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) htmlparser2@6.1.0: dependencies: @@ -33204,7 +32718,7 @@ snapshots: http-proxy@1.18.1(debug@4.4.3(supports-color@10.0.0)): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.0.0)) + follow-redirects: 1.16.2(debug@4.4.3(supports-color@10.0.0)) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -33218,13 +32732,6 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@5.0.1(supports-color@7.2.0): - dependencies: - agent-base: 6.0.2(supports-color@7.2.0) - debug: 4.4.3(supports-color@7.2.0) - transitivePeerDependencies: - - supports-color - https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: agent-base: 6.0.2(supports-color@8.1.1) @@ -33258,8 +32765,6 @@ snapshots: httpxy@0.5.1: {} - human-signals@1.1.1: {} - human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -33300,9 +32805,9 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.14: {} + ignore@7.0.12: {} - ignore@7.0.5: {} + ignore@7.0.14: {} image-meta@0.3.1: {} @@ -33318,12 +32823,12 @@ snapshots: import-meta-resolve@4.2.0: {} - impound@1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + impound@1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 es-module-lexer: 2.1.0 pathe: 2.0.3 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 transitivePeerDependencies: - '@farmfe/core' @@ -33365,22 +32870,6 @@ snapshots: dependencies: tslib: 2.8.1 - inquirer@6.5.2: - dependencies: - ansi-escapes: 3.2.0 - chalk: 2.4.2 - cli-cursor: 2.1.0 - cli-width: 2.2.1 - external-editor: 3.1.0 - figures: 2.0.0 - lodash: 4.18.1 - mute-stream: 0.0.7 - run-async: 2.4.1 - rxjs: 6.6.7 - string-width: 2.1.1 - strip-ansi: 5.2.0 - through: 2.3.8 - inquirer@8.2.4: dependencies: ansi-escapes: 4.3.2 @@ -33448,7 +32937,7 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.7.2: {} + ip-address@10.7.6: {} ipaddr.js@1.9.1: {} @@ -33526,8 +33015,6 @@ snapshots: dependencies: call-bound: 1.0.4 - is-fullwidth-code-point@2.0.0: {} - is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.2: @@ -33722,6 +33209,12 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + istextorbinary@2.1.0: + dependencies: + binaryextensions: 2.3.0 + editions: 1.3.4 + textextensions: 2.6.0 + istextorbinary@2.6.0: dependencies: binaryextensions: 2.3.0 @@ -33807,7 +33300,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 12.0.1 - ws: 8.22.1 + ws: 8.22.2 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -33840,7 +33333,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 12.0.1 - ws: 8.22.1 + ws: 8.22.2 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -33869,34 +33362,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.22.1 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jsdom@26.1.0(supports-color@10.0.0): - dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 - decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2(supports-color@10.0.0) - https-proxy-agent: 7.0.6(supports-color@10.0.0) - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.24 - parse5: 7.3.0 - rrweb-cssom: 0.8.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 5.1.2 - w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.22.1 + ws: 8.22.2 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -33950,8 +33416,6 @@ snapshots: jsonc-parser@3.1.0: {} - jsonc-parser@3.2.0: {} - jsonc-parser@3.3.1: {} jsondiffpatch@0.6.0: @@ -34085,12 +33549,12 @@ snapshots: ky@1.7.5: {} - langchain@1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.1): + langchain@1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.22.2): dependencies: - '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) - '@langchain/langgraph': 1.4.21(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.6.7) - '@langchain/langgraph-checkpoint': 1.1.6(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1)) - langsmith: 0.10.10(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1) + '@langchain/core': 1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) + '@langchain/langgraph': 1.4.21(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@4.6.7) + '@langchain/langgraph-checkpoint': 1.1.6(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2)) + langsmith: 0.10.10(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2) zod: 4.6.7 transitivePeerDependencies: - '@opentelemetry/api' @@ -34100,16 +33564,17 @@ snapshots: - react - react-dom - ws + optional: true - langsmith@0.10.10(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7))(ws@8.22.1): + langsmith@0.10.10(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7))(ws@8.22.2): dependencies: p-queue: 6.6.2 optionalDependencies: '@opentelemetry/api': 1.10.0 - openai: 5.18.1(ws@8.22.1)(zod@4.6.7) - ws: 8.22.1 + openai: 5.18.1(ws@8.22.2)(zod@4.6.7) + ws: 8.22.2 - langsmith@0.3.74(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.1)(zod@4.6.7)): + langsmith@0.3.74(@opentelemetry/api@1.9.0)(openai@5.18.1(ws@8.22.2)(zod@4.6.7)): dependencies: '@types/uuid': 10.0.0 chalk: 4.1.2 @@ -34120,7 +33585,7 @@ snapshots: uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 - openai: 5.18.1(ws@8.22.1)(zod@4.6.7) + openai: 5.18.1(ws@8.22.2)(zod@4.6.7) launch-editor@2.14.2: dependencies: @@ -34133,11 +33598,11 @@ snapshots: dependencies: readable-stream: 2.3.8 - less-loader@11.0.0(less@4.1.3(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + less-loader@11.0.0(less@4.1.3(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: klona: 2.0.6 less: 4.1.3(supports-color@10.0.0) - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) less@4.1.3(supports-color@10.0.0): dependencies: @@ -34195,11 +33660,11 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + license-webpack-plugin@4.0.2(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: webpack-sources: 3.6.0 optionalDependencies: - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) light-my-request@6.6.0: dependencies: @@ -34349,8 +33814,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.omit@4.18.0: {} - lodash.once@4.1.1: {} lodash.sortby@4.7.0: {} @@ -34359,10 +33822,6 @@ snapshots: lodash@4.18.1: {} - log-symbols@2.2.0: - dependencies: - chalk: 2.4.2 - log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -34518,10 +33977,6 @@ snapshots: - bluebird - supports-color - makeerror@1.0.11: - dependencies: - tmpl: 1.0.5 - map-age-cleaner@0.1.3: dependencies: p-defer: 1.0.0 @@ -34931,8 +34386,6 @@ snapshots: mime@4.1.0: {} - mimic-fn@1.2.0: {} - mimic-fn@2.1.0: {} mimic-fn@3.1.0: {} @@ -34943,10 +34396,10 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.6.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + mini-css-extract-plugin@2.6.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: - schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) miniflare@4.20260426.0: dependencies: @@ -34977,7 +34430,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.12 minimatch@11.0.3: dependencies: @@ -35002,41 +34455,41 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)): + minimizer-webpack-plugin@5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) terser: 5.51.2 - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3) optionalDependencies: clean-css: 5.2.2 - cssnano: 7.1.9(postcss@8.5.31) + cssnano: 7.1.9(postcss@8.5.32) csso: 5.0.5 esbuild: 0.28.1 html-minifier-terser: 6.1.0 lightningcss: 1.33.0 - postcss: 8.5.31 + postcss: 8.5.32 sharp: 0.35.5(@types/node@14.18.63) svgo: 4.1.0 uglify-js: 3.13.3 optional: true - minimizer-webpack-plugin@5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + minimizer-webpack-plugin@5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) terser: 5.51.2 - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) optionalDependencies: clean-css: 5.2.2 - cssnano: 7.1.9(postcss@8.5.31) + cssnano: 7.1.9(postcss@8.5.32) csso: 5.0.5 esbuild: 0.28.1 html-minifier-terser: 6.1.0 lightningcss: 1.33.0 - postcss: 8.5.31 + postcss: 8.5.32 sharp: 0.35.5(@types/node@20.19.43) svgo: 4.1.0 uglify-js: 3.13.3 @@ -35144,7 +34597,7 @@ snapshots: camelcase: 6.3.0 debug: 4.4.3(supports-color@10.0.0) find-cache-dir: 3.3.2 - follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.0.0)) + follow-redirects: 1.16.2(debug@4.4.3(supports-color@10.0.0)) https-proxy-agent: 7.0.6(supports-color@10.0.0) mongodb: 7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7) new-find-package-json: 2.0.0(supports-color@10.0.0) @@ -35264,8 +34717,6 @@ snapshots: mustache@4.2.0: {} - mute-stream@0.0.7: {} - mute-stream@0.0.8: {} mysql2@3.23.3(@types/node@20.19.43): @@ -35292,7 +34743,7 @@ snapshots: nanoid@3.3.20: {} - nanoid@5.1.9: {} + nanoid@5.1.16: {} nanospinner@1.2.2: dependencies: @@ -35388,9 +34839,9 @@ snapshots: jsonc-parser: 3.3.1 less: 4.2.0(supports-color@10.0.0) ora: 5.4.1 - postcss: 8.5.31 - postcss-preset-env: 7.8.3(postcss@8.5.31) - postcss-url: 10.1.3(postcss@8.5.31) + postcss: 8.5.32 + postcss-preset-env: 7.8.3(postcss@8.5.32) + postcss-url: 10.1.3(postcss@8.5.32) rollup: 2.80.0 rollup-plugin-sourcemaps: 0.6.3(@types/node@14.18.63)(rollup@2.80.0) rxjs: 7.8.2 @@ -35411,7 +34862,7 @@ snapshots: node-gyp-build: 4.6.0 optional: true - nitro@3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + nitro@3.0.260311-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -35432,7 +34883,7 @@ snapshots: giget: 3.3.1 jiti: 2.7.0 rollup: 4.62.3 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -35464,7 +34915,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + nitro@3.0.260415-beta(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(lru-cache@11.4.0)(miniflare@4.20260426.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(mysql2@3.23.3(@types/node@20.19.43))(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.10(srvx@0.11.22) @@ -35485,7 +34936,7 @@ snapshots: giget: 3.3.1 jiti: 2.7.0 rollup: 4.62.3 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -35517,7 +34968,7 @@ snapshots: - sqlite3 - uploadthing - nitropack@2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.143.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + nitropack@2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.143.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.62.3) @@ -35582,7 +35033,7 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0)) untyped: 2.0.0 @@ -35626,7 +35077,7 @@ snapshots: - vite - webpack - nitropack@2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + nitropack@2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.62.3) @@ -35691,7 +35142,7 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0)) untyped: 2.0.0 @@ -35735,7 +35186,7 @@ snapshots: - vite - webpack - nitropack@2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + nitropack@2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.62.3) @@ -35800,7 +35251,7 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0)) untyped: 2.0.0 @@ -35913,8 +35364,6 @@ snapshots: - bluebird - supports-color - node-int64@0.4.0: {} - node-mock-http@1.0.4: {} node-releases@2.0.59: {} @@ -36060,16 +35509,16 @@ snapshots: nuxi@3.25.1: {} - nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3): + nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5): dependencies: - '@dxup/nuxt': 0.5.6(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(esbuild@0.28.1)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + '@dxup/nuxt': 0.5.6(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(esbuild@0.28.1)(giget@3.3.1)(jiti@2.7.0)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) '@nuxt/cli': 3.37.0(@nuxt/schema@3.21.11)(@parcel/watcher@2.5.6)(cac@6.7.14)(commander@14.0.3)(magicast@0.5.5)(supports-color@10.0.0) - '@nuxt/devtools': 3.4.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) + '@nuxt/devtools': 3.4.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@nuxt/schema@3.21.11)(confbox@0.2.4)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(dotenv@17.4.2)(giget@3.3.1)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)))(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)) '@nuxt/kit': 3.21.11(magicast@0.5.5) - '@nuxt/nitro-server': 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3))(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + '@nuxt/nitro-server': 3.21.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(encoding@0.1.13)(esbuild@0.28.1)(ioredis@5.11.1(supports-color@10.0.0))(magicast@0.5.5)(mysql2@3.23.3(@types/node@20.19.43))(nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5))(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(srvx@0.11.22)(supports-color@10.0.0)(typescript@7.0.2)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) '@nuxt/schema': 3.21.11 '@nuxt/telemetry': 2.8.0(@nuxt/kit@3.21.11(magicast@0.5.5)) - '@nuxt/vite-builder': 3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.5)(meow@13.2.0)(nuxt@3.21.11(8ad65ef44c1ddb5619f9223b1cfc84d3))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.16)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.16)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0) + '@nuxt/vite-builder': 3.21.11(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(magicast@0.5.5)(meow@13.2.0)(nuxt@3.21.11(724864ca76bc73825b6249c96f34efd5))(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(rolldown@1.2.16)(rollup-plugin-visualizer@6.0.11(rolldown@1.2.16)(rollup@4.62.3))(rollup@4.62.3)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(typescript@7.0.2)(vue@3.5.44(typescript@7.0.2))(yaml@2.9.0) '@unhead/vue': 2.1.17(vue@3.5.44(typescript@7.0.2)) '@vue/shared': 3.5.44 c12: 3.3.4(magicast@0.5.5) @@ -36086,7 +35535,7 @@ snapshots: h3: 1.15.11 hookable: 5.5.3 ignore: 7.0.14 - impound: 1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + impound: 1.1.6(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) jiti: 2.7.0 klona: 2.0.6 knitwork: 1.3.0 @@ -36113,8 +35562,8 @@ snapshots: ultrahtml: 1.7.0 uncrypto: 0.1.3 unctx: 2.5.0 - unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unimport: 6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-vue-router: 0.19.2(@vue/compiler-sfc@3.5.44)(vue-router@4.6.4(vue@3.5.44(typescript@7.0.2)))(vue@3.5.44(typescript@7.0.2)) untyped: 2.0.0 vue: 3.5.44(typescript@7.0.2) @@ -36193,8 +35642,10 @@ snapshots: nwsapi@2.2.24: {} - nx@22.7.10: + nx@24.0.3: dependencies: + '@clack/core': 1.4.3 + '@clack/prompts': 1.7.0 '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 '@emnapi/wasi-threads': 1.0.4 @@ -36203,19 +35654,14 @@ snapshots: '@tybys/wasm-util': 0.9.0 '@yarnpkg/lockfile': 1.1.0 '@zkochan/js-yaml': 0.0.7 - agent-base: 6.0.2(supports-color@7.2.0) - ansi-colors: 4.1.3 ansi-regex: 5.0.1 ansi-styles: 4.3.0 argparse: 2.0.1 - asynckit: 0.4.0 - axios: 1.18.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) balanced-match: 4.0.3 base64-js: 1.5.1 bl: 4.1.0 - brace-expansion: 5.0.8 + brace-expansion: 5.0.12 buffer: 5.7.1 - call-bind-apply-helpers: 1.0.2 chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 @@ -36223,74 +35669,50 @@ snapshots: clone: 1.0.4 color-convert: 2.0.1 color-name: 1.1.4 - combined-stream: 1.0.8 - debug: 4.4.3(supports-color@7.2.0) defaults: 1.0.4 - define-lazy-prop: 2.0.0 - delayed-stream: 1.0.0 dotenv: 16.4.7 dotenv-expand: 12.0.3 - dunder-proto: 1.0.1 ejs: 5.0.1 emoji-regex: 8.0.0 end-of-stream: 1.4.5 - enquirer: 2.3.6 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 escalade: 3.2.0 escape-string-regexp: 1.0.5 + fast-string-truncated-width: 3.0.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 figures: 3.2.0 - flat: 5.0.2 - follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) - form-data: 4.0.6 fs-constants: 1.0.0 - function-bind: 1.1.2 get-caller-file: 2.0.5 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 has-flag: 4.0.0 - has-symbols: 1.1.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - https-proxy-agent: 5.0.1(supports-color@7.2.0) ieee754: 1.2.1 - ignore: 7.0.5 + ignore: 7.0.12 inherits: 2.0.4 - is-docker: 2.2.1 is-fullwidth-code-point: 3.0.0 is-interactive: 1.0.0 is-unicode-supported: 0.1.0 - is-wsl: 2.2.0 + isexe: 2.0.0 json5: 2.2.3 - jsonc-parser: 3.2.0 + jsonc-parser: 3.3.1 lines-and-columns: 2.0.3 log-symbols: 4.1.0 - math-intrinsics: 1.1.0 - mime-db: 1.52.0 - mime-types: 2.1.35 mimic-fn: 2.1.0 minimatch: 10.2.5 minimist: 1.2.8 - ms: 2.1.3 npm-run-path: 4.0.1 once: 1.4.0 onetime: 5.1.2 - open: 8.4.2 - ora: 5.3.0 + ora: 5.4.1 path-key: 3.1.1 picocolors: 1.1.1 - proxy-from-env: 2.1.0 readable-stream: 3.6.2 require-directory: 2.1.1 resolve.exports: 2.0.3 restore-cursor: 3.1.0 safe-buffer: 5.2.1 - semver: 7.7.4 + semver: 7.8.5 signal-exit: 3.0.7 - smol-toml: 1.6.1 + sisteransi: 1.0.5 + smol-toml: 1.9.0 string-width: 4.2.3 string_decoder: 1.3.0 strip-ansi: 6.0.1 @@ -36298,11 +35720,12 @@ snapshots: supports-color: 7.2.0 tar-stream: 2.2.0 tmp: 0.2.7 - tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tslib: 2.8.1 + undici: 7.30.1 util-deprecate: 1.0.2 wcwidth: 1.0.1 + which: 3.0.1 wrap-ansi: 7.0.0 wrappy: 1.0.2 y18n: 5.0.8 @@ -36310,16 +35733,16 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 22.7.10 - '@nx/nx-darwin-x64': 22.7.10 - '@nx/nx-freebsd-x64': 22.7.10 - '@nx/nx-linux-arm-gnueabihf': 22.7.10 - '@nx/nx-linux-arm64-gnu': 22.7.10 - '@nx/nx-linux-arm64-musl': 22.7.10 - '@nx/nx-linux-x64-gnu': 22.7.10 - '@nx/nx-linux-x64-musl': 22.7.10 - '@nx/nx-win32-arm64-msvc': 22.7.10 - '@nx/nx-win32-x64-msvc': 22.7.10 + '@nx/nx-darwin-arm64': 24.0.3 + '@nx/nx-darwin-x64': 24.0.3 + '@nx/nx-freebsd-x64': 24.0.3 + '@nx/nx-linux-arm-gnueabihf': 24.0.3 + '@nx/nx-linux-arm64-gnu': 24.0.3 + '@nx/nx-linux-arm64-musl': 24.0.3 + '@nx/nx-linux-x64-gnu': 24.0.3 + '@nx/nx-linux-x64-musl': 24.0.3 + '@nx/nx-win32-arm64-msvc': 24.0.3 + '@nx/nx-win32-x64-msvc': 24.0.3 nypm@0.6.9: dependencies: @@ -36395,10 +35818,6 @@ snapshots: on-exit-leak-free@2.1.2: {} - on-finished@2.3.0: - dependencies: - ee-first: 1.1.1 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -36413,10 +35832,6 @@ snapshots: dependencies: fn.name: 1.1.0 - onetime@2.0.1: - dependencies: - mimic-fn: 1.2.0 - onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -36458,30 +35873,30 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openai@5.18.1(ws@8.22.1)(zod@3.25.76): + openai@5.18.1(ws@8.22.2)(zod@3.25.76): optionalDependencies: - ws: 8.22.1 + ws: 8.22.2 zod: 3.25.76 - openai@5.18.1(ws@8.22.1)(zod@4.6.7): + openai@5.18.1(ws@8.22.2)(zod@4.6.7): optionalDependencies: - ws: 8.22.1 + ws: 8.22.2 zod: 4.6.7 - openai@7.19.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7): + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7): optionalDependencies: '@aws-sdk/credential-provider-node': 3.972.92 '@smithy/signature-v4': 5.7.5 undici: 7.24.8 - ws: 8.22.1 + ws: 8.22.2 zod: 4.6.7 - openai@7.31.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.1)(zod@4.6.7): + openai@7.31.0(@aws-sdk/credential-provider-node@3.972.92)(@smithy/signature-v4@5.7.5)(undici@7.24.8)(ws@8.22.2)(zod@4.6.7): optionalDependencies: '@aws-sdk/credential-provider-node': 3.972.92 '@smithy/signature-v4': 5.7.5 undici: 7.24.8 - ws: 8.22.1 + ws: 8.22.2 zod: 4.6.7 optional-require@1.1.8: @@ -36497,26 +35912,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@3.4.0: - dependencies: - chalk: 2.4.2 - cli-cursor: 2.1.0 - cli-spinners: 2.9.2 - log-symbols: 2.2.0 - strip-ansi: 5.2.0 - wcwidth: 1.0.1 - - ora@5.3.0: - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.6.1 - is-interactive: 1.0.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - ora@5.4.1: dependencies: bl: 4.1.0 @@ -36849,8 +36244,6 @@ snapshots: parse-node-version@1.0.1: {} - parse-passwd@1.0.0: {} - parse-statements@1.0.11: {} parse5-html-rewriting-stream@6.0.1: @@ -36920,7 +36313,7 @@ snapshots: lru-cache: 11.4.0 minipass: 7.1.3 - path-to-regexp@0.1.12: {} + path-to-regexp@0.1.13: {} path-to-regexp@1.9.0: dependencies: @@ -37100,17 +36493,17 @@ snapshots: postcss-attribute-case-insensitive@5.0.2(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-attribute-case-insensitive@5.0.2(postcss@8.5.31): + postcss-attribute-case-insensitive@5.0.2(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - postcss-calc@10.1.1(postcss@8.5.31): + postcss-calc@10.1.1(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss: 8.5.32 + postcss-selector-parser: 7.2.0 postcss-value-parser: 4.2.0 postcss-clamp@4.1.0(postcss@8.4.31): @@ -37118,9 +36511,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.31): + postcss-clamp@4.1.0(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-color-functional-notation@4.2.4(postcss@8.4.31): @@ -37128,9 +36521,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@4.2.4(postcss@8.5.31): + postcss-color-functional-notation@4.2.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-color-hex-alpha@8.0.4(postcss@8.4.31): @@ -37138,9 +36531,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-color-hex-alpha@8.0.4(postcss@8.5.31): + postcss-color-hex-alpha@8.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-color-rebeccapurple@7.1.1(postcss@8.4.31): @@ -37148,23 +36541,23 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@7.1.1(postcss@8.5.31): + postcss-color-rebeccapurple@7.1.1(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.10(postcss@8.5.31): + postcss-colormin@7.0.10(postcss@8.5.32): dependencies: '@colordx/core': 5.5.0 browserslist: 4.29.0 caniuse-api: 3.0.0 - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.12(postcss@8.5.31): + postcss-convert-values@7.0.12(postcss@8.5.32): dependencies: browserslist: 4.29.0 - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-custom-media@8.0.2(postcss@8.4.31): @@ -37172,9 +36565,9 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-custom-media@8.0.2(postcss@8.5.31): + postcss-custom-media@8.0.2(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-custom-properties@12.1.11(postcss@8.4.31): @@ -37182,47 +36575,47 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-custom-properties@12.1.11(postcss@8.5.31): + postcss-custom-properties@12.1.11(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-custom-selectors@6.0.3(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-custom-selectors@6.0.3(postcss@8.5.31): + postcss-custom-selectors@6.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 postcss-dir-pseudo-class@6.0.5(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-dir-pseudo-class@6.0.5(postcss@8.5.31): + postcss-dir-pseudo-class@6.0.5(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - postcss-discard-comments@7.0.8(postcss@8.5.31): + postcss-discard-comments@7.0.8(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss: 8.5.32 + postcss-selector-parser: 7.2.0 - postcss-discard-duplicates@7.0.4(postcss@8.5.31): + postcss-discard-duplicates@7.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 - postcss-discard-empty@7.0.3(postcss@8.5.31): + postcss-discard-empty@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 - postcss-discard-overridden@7.0.3(postcss@8.5.31): + postcss-discard-overridden@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-double-position-gradients@3.1.2(postcss@8.4.31): dependencies: @@ -37230,10 +36623,10 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-double-position-gradients@3.1.2(postcss@8.5.31): + postcss-double-position-gradients@3.1.2(postcss@8.5.32): dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.31) - postcss: 8.5.31 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-env-function@4.0.6(postcss@8.4.31): @@ -37241,55 +36634,55 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-env-function@4.0.6(postcss@8.5.31): + postcss-env-function@4.0.6(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-focus-visible@6.0.4(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-focus-visible@6.0.4(postcss@8.5.31): + postcss-focus-visible@6.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 postcss-focus-within@5.0.4(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-focus-within@5.0.4(postcss@8.5.31): + postcss-focus-within@5.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 postcss-font-variant@5.0.0(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-font-variant@5.0.0(postcss@8.5.31): + postcss-font-variant@5.0.0(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-gap-properties@3.0.5(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-gap-properties@3.0.5(postcss@8.5.31): + postcss-gap-properties@3.0.5(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-image-set-function@4.0.7(postcss@8.4.31): dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-image-set-function@4.0.7(postcss@8.5.31): + postcss-image-set-function@4.0.7(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-import@15.0.0(postcss@8.4.31): @@ -37303,9 +36696,9 @@ snapshots: dependencies: postcss: 8.4.31 - postcss-initial@4.0.1(postcss@8.5.31): + postcss-initial@4.0.1(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-lab-function@4.2.1(postcss@8.4.31): dependencies: @@ -37313,76 +36706,76 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-lab-function@4.2.1(postcss@8.5.31): + postcss-lab-function@4.2.1(postcss@8.5.32): dependencies: - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.31) - postcss: 8.5.31 + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-loader@7.0.1(postcss@8.4.31)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + postcss-loader@7.0.1(postcss@8.4.31)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: cosmiconfig: 7.1.0 klona: 2.0.6 postcss: 8.4.31 semver: 7.8.5 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) postcss-logical@5.0.4(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-logical@5.0.4(postcss@8.5.31): + postcss-logical@5.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-media-minmax@5.0.0(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-media-minmax@5.0.0(postcss@8.5.31): + postcss-media-minmax@5.0.0(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 - postcss-merge-longhand@7.0.7(postcss@8.5.31): + postcss-merge-longhand@7.0.7(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - stylehacks: 7.0.11(postcss@8.5.31) + stylehacks: 7.0.11(postcss@8.5.32) - postcss-merge-rules@7.0.11(postcss@8.5.31): + postcss-merge-rules@7.0.11(postcss@8.5.32): dependencies: browserslist: 4.29.0 caniuse-api: 3.0.0 - cssnano-utils: 5.0.3(postcss@8.5.31) - postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + cssnano-utils: 5.0.3(postcss@8.5.32) + postcss: 8.5.32 + postcss-selector-parser: 7.2.0 - postcss-minify-font-values@7.0.3(postcss@8.5.31): + postcss-minify-font-values@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.5(postcss@8.5.31): + postcss-minify-gradients@7.0.5(postcss@8.5.32): dependencies: '@colordx/core': 5.5.0 - cssnano-utils: 5.0.3(postcss@8.5.31) - postcss: 8.5.31 + cssnano-utils: 5.0.3(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.9(postcss@8.5.31): + postcss-minify-params@7.0.9(postcss@8.5.32): dependencies: browserslist: 4.29.0 - cssnano-utils: 5.0.3(postcss@8.5.31) - postcss: 8.5.31 + cssnano-utils: 5.0.3(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.1.2(postcss@8.5.31): + postcss-minify-selectors@7.1.2(postcss@8.5.32): dependencies: browserslist: 4.29.0 caniuse-api: 3.0.0 cssesc: 3.0.0 - postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss: 8.5.32 + postcss-selector-parser: 7.2.0 postcss-modules-extract-imports@3.0.0(postcss@8.4.31): dependencies: @@ -37392,13 +36785,13 @@ snapshots: dependencies: icss-utils: 5.1.0(postcss@8.4.31) postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-value-parser: 4.2.0 postcss-modules-scope@3.0.0(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-modules-values@4.0.0(postcss@8.4.31): dependencies: @@ -37407,73 +36800,73 @@ snapshots: postcss-nesting@10.2.0(postcss@8.4.31): dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.4) + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.6) postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-nesting@10.2.0(postcss@8.5.31): + postcss-nesting@10.2.0(postcss@8.5.32): dependencies: - '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.4) - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.6) + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - postcss-normalize-charset@7.0.3(postcss@8.5.31): + postcss-normalize-charset@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 - postcss-normalize-display-values@7.0.3(postcss@8.5.31): + postcss-normalize-display-values@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.4(postcss@8.5.31): + postcss-normalize-positions@7.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.4(postcss@8.5.31): + postcss-normalize-repeat-style@7.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.3(postcss@8.5.31): + postcss-normalize-string@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.3(postcss@8.5.31): + postcss-normalize-timing-functions@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.9(postcss@8.5.31): + postcss-normalize-unicode@7.0.9(postcss@8.5.32): dependencies: browserslist: 4.29.0 - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.3(postcss@8.5.31): + postcss-normalize-url@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.3(postcss@8.5.31): + postcss-normalize-whitespace@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-opacity-percentage@1.1.3(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-opacity-percentage@1.1.3(postcss@8.5.31): + postcss-opacity-percentage@1.1.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 - postcss-ordered-values@7.0.4(postcss@8.5.31): + postcss-ordered-values@7.0.4(postcss@8.5.32): dependencies: - cssnano-utils: 5.0.3(postcss@8.5.31) - postcss: 8.5.31 + cssnano-utils: 5.0.3(postcss@8.5.32) + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-overflow-shorthand@3.0.4(postcss@8.4.31): @@ -37481,27 +36874,27 @@ snapshots: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@3.0.4(postcss@8.5.31): + postcss-overflow-shorthand@3.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-page-break@3.0.4(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-page-break@3.0.4(postcss@8.5.31): + postcss-page-break@3.0.4(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-place@7.0.5(postcss@8.4.31): dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 - postcss-place@7.0.5(postcss@8.5.31): + postcss-place@7.0.5(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-preset-env@7.8.0(postcss@8.4.31): @@ -37557,134 +36950,132 @@ snapshots: postcss-selector-not: 6.0.1(postcss@8.4.31) postcss-value-parser: 4.2.0 - postcss-preset-env@7.8.3(postcss@8.5.31): - dependencies: - '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.5.31) - '@csstools/postcss-color-function': 1.1.1(postcss@8.5.31) - '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.5.31) - '@csstools/postcss-hwb-function': 1.0.2(postcss@8.5.31) - '@csstools/postcss-ic-unit': 1.0.1(postcss@8.5.31) - '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.5.31) - '@csstools/postcss-nested-calc': 1.0.0(postcss@8.5.31) - '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.5.31) - '@csstools/postcss-oklab-function': 1.1.1(postcss@8.5.31) - '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.31) - '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.5.31) - '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.5.31) - '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.5.31) - '@csstools/postcss-unset-value': 1.0.2(postcss@8.5.31) - autoprefixer: 10.5.4(postcss@8.5.31) + postcss-preset-env@7.8.3(postcss@8.5.32): + dependencies: + '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.5.32) + '@csstools/postcss-color-function': 1.1.1(postcss@8.5.32) + '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.5.32) + '@csstools/postcss-hwb-function': 1.0.2(postcss@8.5.32) + '@csstools/postcss-ic-unit': 1.0.1(postcss@8.5.32) + '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.5.32) + '@csstools/postcss-nested-calc': 1.0.0(postcss@8.5.32) + '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.5.32) + '@csstools/postcss-oklab-function': 1.1.1(postcss@8.5.32) + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.32) + '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.5.32) + '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.5.32) + '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.5.32) + '@csstools/postcss-unset-value': 1.0.2(postcss@8.5.32) + autoprefixer: 10.5.4(postcss@8.5.32) browserslist: 4.29.0 - css-blank-pseudo: 3.0.3(postcss@8.5.31) - css-has-pseudo: 3.0.4(postcss@8.5.31) - css-prefers-color-scheme: 6.0.3(postcss@8.5.31) + css-blank-pseudo: 3.0.3(postcss@8.5.32) + css-has-pseudo: 3.0.4(postcss@8.5.32) + css-prefers-color-scheme: 6.0.3(postcss@8.5.32) cssdb: 7.11.2 - postcss: 8.5.31 - postcss-attribute-case-insensitive: 5.0.2(postcss@8.5.31) - postcss-clamp: 4.1.0(postcss@8.5.31) - postcss-color-functional-notation: 4.2.4(postcss@8.5.31) - postcss-color-hex-alpha: 8.0.4(postcss@8.5.31) - postcss-color-rebeccapurple: 7.1.1(postcss@8.5.31) - postcss-custom-media: 8.0.2(postcss@8.5.31) - postcss-custom-properties: 12.1.11(postcss@8.5.31) - postcss-custom-selectors: 6.0.3(postcss@8.5.31) - postcss-dir-pseudo-class: 6.0.5(postcss@8.5.31) - postcss-double-position-gradients: 3.1.2(postcss@8.5.31) - postcss-env-function: 4.0.6(postcss@8.5.31) - postcss-focus-visible: 6.0.4(postcss@8.5.31) - postcss-focus-within: 5.0.4(postcss@8.5.31) - postcss-font-variant: 5.0.0(postcss@8.5.31) - postcss-gap-properties: 3.0.5(postcss@8.5.31) - postcss-image-set-function: 4.0.7(postcss@8.5.31) - postcss-initial: 4.0.1(postcss@8.5.31) - postcss-lab-function: 4.2.1(postcss@8.5.31) - postcss-logical: 5.0.4(postcss@8.5.31) - postcss-media-minmax: 5.0.0(postcss@8.5.31) - postcss-nesting: 10.2.0(postcss@8.5.31) - postcss-opacity-percentage: 1.1.3(postcss@8.5.31) - postcss-overflow-shorthand: 3.0.4(postcss@8.5.31) - postcss-page-break: 3.0.4(postcss@8.5.31) - postcss-place: 7.0.5(postcss@8.5.31) - postcss-pseudo-class-any-link: 7.1.6(postcss@8.5.31) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.31) - postcss-selector-not: 6.0.1(postcss@8.5.31) + postcss: 8.5.32 + postcss-attribute-case-insensitive: 5.0.2(postcss@8.5.32) + postcss-clamp: 4.1.0(postcss@8.5.32) + postcss-color-functional-notation: 4.2.4(postcss@8.5.32) + postcss-color-hex-alpha: 8.0.4(postcss@8.5.32) + postcss-color-rebeccapurple: 7.1.1(postcss@8.5.32) + postcss-custom-media: 8.0.2(postcss@8.5.32) + postcss-custom-properties: 12.1.11(postcss@8.5.32) + postcss-custom-selectors: 6.0.3(postcss@8.5.32) + postcss-dir-pseudo-class: 6.0.5(postcss@8.5.32) + postcss-double-position-gradients: 3.1.2(postcss@8.5.32) + postcss-env-function: 4.0.6(postcss@8.5.32) + postcss-focus-visible: 6.0.4(postcss@8.5.32) + postcss-focus-within: 5.0.4(postcss@8.5.32) + postcss-font-variant: 5.0.0(postcss@8.5.32) + postcss-gap-properties: 3.0.5(postcss@8.5.32) + postcss-image-set-function: 4.0.7(postcss@8.5.32) + postcss-initial: 4.0.1(postcss@8.5.32) + postcss-lab-function: 4.2.1(postcss@8.5.32) + postcss-logical: 5.0.4(postcss@8.5.32) + postcss-media-minmax: 5.0.0(postcss@8.5.32) + postcss-nesting: 10.2.0(postcss@8.5.32) + postcss-opacity-percentage: 1.1.3(postcss@8.5.32) + postcss-overflow-shorthand: 3.0.4(postcss@8.5.32) + postcss-page-break: 3.0.4(postcss@8.5.32) + postcss-place: 7.0.5(postcss@8.5.32) + postcss-pseudo-class-any-link: 7.1.6(postcss@8.5.32) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.32) + postcss-selector-not: 6.0.1(postcss@8.5.32) postcss-value-parser: 4.2.0 postcss-pseudo-class-any-link@7.1.6(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-pseudo-class-any-link@7.1.6(postcss@8.5.31): + postcss-pseudo-class-any-link@7.1.6(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - postcss-reduce-initial@7.0.9(postcss@8.5.31): + postcss-reduce-initial@7.0.9(postcss@8.5.32): dependencies: browserslist: 4.29.0 caniuse-api: 3.0.0 - postcss: 8.5.31 + postcss: 8.5.32 - postcss-reduce-transforms@7.0.3(postcss@8.5.31): + postcss-reduce-transforms@7.0.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 postcss-replace-overflow-wrap@4.0.0(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.31): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-selector-not@6.0.1(postcss@8.4.31): dependencies: postcss: 8.4.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 - postcss-selector-not@6.0.1(postcss@8.5.31): + postcss-selector-not@6.0.1(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss: 8.5.32 + postcss-selector-parser: 6.1.6 - postcss-selector-parser@6.1.4: + postcss-selector-parser@6.1.6: dependencies: - cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.5: + postcss-selector-parser@7.2.0: dependencies: - cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.1.3(postcss@8.5.31): + postcss-svgo@7.1.3(postcss@8.5.32): dependencies: - postcss: 8.5.31 + postcss: 8.5.32 postcss-value-parser: 4.2.0 svgo: 4.1.0 - postcss-unique-selectors@7.0.7(postcss@8.5.31): + postcss-unique-selectors@7.0.7(postcss@8.5.32): dependencies: - postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss: 8.5.32 + postcss-selector-parser: 7.2.0 - postcss-url@10.1.3(postcss@8.5.31): + postcss-url@10.1.3(postcss@8.5.32): dependencies: make-dir: 3.1.0 mime: 2.5.2 minimatch: 3.1.5 - postcss: 8.5.31 + postcss: 8.5.32 xxhashjs: 0.2.2 postcss-value-parser@4.2.0: {} - postcss-values-parser@6.0.2(postcss@8.5.31): + postcss-values-parser@6.0.2(postcss@8.5.32): dependencies: color-name: 1.1.4 is-url-superb: 4.0.0 - postcss: 8.5.31 + postcss: 8.5.32 quote-unquote: 1.0.0 postcss@8.4.31: @@ -37693,7 +37084,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 - postcss@8.5.31: + postcss@8.5.32: dependencies: nanoid: 3.3.20 picocolors: 1.1.1 @@ -37724,7 +37115,7 @@ snapshots: detective-amd: 6.0.1 detective-cjs: 6.0.1 detective-es6: 5.0.1 - detective-postcss: 7.0.1(postcss@8.5.31) + detective-postcss: 7.0.1(postcss@8.5.32) detective-sass: 6.0.1 detective-scss: 5.0.1 detective-stylus: 5.0.1 @@ -37732,7 +37123,7 @@ snapshots: detective-vue2: 2.2.0(supports-color@10.0.0)(typescript@5.8.3) module-definition: 6.0.1 node-source-walk: 7.0.1 - postcss: 8.5.31 + postcss: 8.5.32 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -37745,8 +37136,6 @@ snapshots: prelude-ls@1.2.1: {} - premove@4.0.0: {} - prettier@2.8.8: {} prettier@3.9.10: {} @@ -37786,7 +37175,7 @@ snapshots: transitivePeerDependencies: - magicast - prismjs@1.29.0: {} + prismjs@1.30.0: {} private@0.1.8: {} @@ -37871,20 +37260,16 @@ snapshots: pstree.remy@1.1.8: {} - pump@3.0.0: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - punycode@2.3.1: {} pure-rand@6.1.0: {} - qs@6.14.2: + qs@6.16.2: dependencies: + es-define-property: 1.0.1 side-channel: 1.1.1 - qs@6.15.3: + qs@6.17.1: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -37988,7 +37373,7 @@ snapshots: react-router@4.1.0(react@18.3.1): dependencies: - history: 4.9.0 + history: 4.6.0 hoist-non-react-statics: 1.2.0 invariant: 2.2.4 loose-envify: 1.4.0 @@ -38188,8 +37573,6 @@ snapshots: regenerate@1.4.2: {} - regenerator-runtime@0.13.10: {} - regenerator-runtime@0.13.9: {} regex-parser@2.2.11: {} @@ -38300,7 +37683,7 @@ snapshots: remove-types@1.0.0(supports-color@10.0.0): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) - '@babel/plugin-syntax-decorators': 7.25.9(@babel/core@7.29.7(supports-color@10.0.0)) + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0))(supports-color@10.0.0) prettier: 2.8.8 transitivePeerDependencies: @@ -38335,11 +37718,6 @@ snapshots: resolve-dependency-path@4.0.1: {} - resolve-dir@1.0.1: - dependencies: - expand-tilde: 2.0.2 - global-modules: 1.0.0 - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -38358,11 +37736,6 @@ snapshots: dependencies: path-root: 0.1.1 - resolve-path@1.4.0: - dependencies: - http-errors: 1.6.3 - path-is-absolute: 1.0.1 - resolve-pathname@2.2.0: {} resolve-pkg-maps@1.0.0: {} @@ -38390,11 +37763,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - restore-cursor@2.0.0: - dependencies: - onetime: 2.0.1 - signal-exit: 3.0.7 - restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -38454,11 +37822,6 @@ snapshots: dependencies: glob: 7.2.3 - rimraf@6.1.3: - dependencies: - glob: 13.0.6 - package-json-from-dist: 1.0.1 - rolldown@1.2.16: dependencies: '@oxc-project/types': 0.156.0 @@ -38644,28 +38007,16 @@ snapshots: safer-buffer@2.1.2: {} - sane@5.0.1: - dependencies: - '@cnakazawa/watch': 1.0.4 - anymatch: 3.1.3 - capture-exit: 2.0.0 - exec-sh: 0.3.6 - execa: 4.1.0 - fb-watchman: 2.0.2 - micromatch: 4.0.8 - minimist: 1.2.8 - walker: 1.0.7 - saslprep@1.0.3: dependencies: sparse-bitfield: 3.0.3 optional: true - sass-loader@13.0.2(sass@1.54.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + sass-loader@13.0.2(sass@1.54.4)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: klona: 2.0.6 neo-async: 2.6.2 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) optionalDependencies: sass: 1.54.4 @@ -38710,33 +38061,33 @@ snapshots: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - schema-utils@4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)): + schema-utils@4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@types/json-schema': 7.0.15 ajv: 8.20.0 ajv-formats: 3.0.1 ajv-keywords: 5.1.0(ajv@8.20.0) optionalDependencies: - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3) optional: true - schema-utils@4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + schema-utils@4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@types/json-schema': 7.0.15 ajv: 8.20.0 ajv-formats: 3.0.1 ajv-keywords: 5.1.0(ajv@8.20.0) optionalDependencies: - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) - schema-utils@4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + schema-utils@4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: '@types/json-schema': 7.0.15 ajv: 8.20.0 ajv-formats: 3.0.1 ajv-keywords: 5.1.0(ajv@8.20.0) optionalDependencies: - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) scule@1.3.0: {} @@ -38766,8 +38117,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.7.4: {} - semver@7.8.5: {} send@0.19.2(supports-color@10.0.0): @@ -38804,9 +38153,7 @@ snapshots: transitivePeerDependencies: - supports-color - sentry@0.44.0: {} - - sentry@0.45.0: {} + sentry@0.49.0: {} serialize-javascript@6.0.2: dependencies: @@ -39125,7 +38472,7 @@ snapshots: smob@1.4.1: {} - smol-toml@1.6.1: {} + smol-toml@1.9.0: {} sockjs@0.3.24: dependencies: @@ -39143,7 +38490,7 @@ snapshots: socks@2.8.7: dependencies: - ip-address: 10.7.2 + ip-address: 10.7.6 smart-buffer: 4.2.0 solid-js@1.9.18: @@ -39179,12 +38526,12 @@ snapshots: source-map-js@1.2.2: {} - source-map-loader@4.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + source-map-loader@4.0.0(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.2 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) source-map-resolve@0.6.0: dependencies: @@ -39349,11 +38696,6 @@ snapshots: strict-uri-encode@1.1.0: {} - string-width@2.1.1: - dependencies: - is-fullwidth-code-point: 2.0.0 - strip-ansi: 4.0.0 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -39421,14 +38763,6 @@ snapshots: is-obj: 1.0.1 is-regexp: 1.0.0 - strip-ansi@4.0.0: - dependencies: - ansi-regex: 3.0.1 - - strip-ansi@5.2.0: - dependencies: - ansi-regex: 4.1.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -39476,19 +38810,19 @@ snapshots: optionalDependencies: '@babel/core': 7.29.7(supports-color@10.0.0) - stylehacks@7.0.11(postcss@8.5.31): + stylehacks@7.0.11(postcss@8.5.32): dependencies: browserslist: 4.29.0 - postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss: 8.5.32 + postcss-selector-parser: 7.2.0 - stylus-loader@7.0.0(stylus@0.59.0(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + stylus-loader@7.0.0(stylus@0.59.0(supports-color@10.0.0))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: fast-glob: 3.3.3 klona: 2.0.6 normalize-path: 3.0.0 stylus: 0.59.0(supports-color@10.0.0) - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) stylus-lookup@6.1.0: dependencies: @@ -39659,16 +38993,16 @@ snapshots: solid-js: 1.9.18 solid-use: 0.8.0(solid-js@1.9.18) - terser-webpack-plugin@5.6.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + terser-webpack-plugin@5.6.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 - schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) terser: 5.51.2 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) optionalDependencies: clean-css: 5.2.2 - cssnano: 7.1.9(postcss@8.5.31) + cssnano: 7.1.9(postcss@8.5.32) csso: 5.0.5 esbuild: 0.15.5 html-minifier-terser: 6.1.0 @@ -39716,11 +39050,6 @@ snapshots: throttleit@2.1.0: {} - through2@3.0.2: - dependencies: - inherits: 2.0.4 - readable-stream: 3.6.2 - through@2.3.8: {} thunky@1.1.0: {} @@ -39767,14 +39096,8 @@ snapshots: dependencies: os-tmpdir: 1.0.2 - tmp@0.1.0: - dependencies: - rimraf: 2.7.1 - tmp@0.2.7: {} - tmpl@1.0.5: {} - to-buffer@1.2.2: dependencies: isarray: 2.0.5 @@ -39842,16 +39165,6 @@ snapshots: transitivePeerDependencies: - supports-color - tree-sync@2.1.0(supports-color@10.0.0): - dependencies: - debug: 4.4.3(supports-color@10.0.0) - fs-tree-diff: 2.0.1(supports-color@10.0.0) - mkdirp: 0.5.6 - quick-temp: 0.1.8 - walk-sync: 0.3.4 - transitivePeerDependencies: - - supports-color - trim-lines@3.0.1: {} triple-beam@1.4.1: {} @@ -39942,7 +39255,7 @@ snapshots: type-is@2.1.0: dependencies: - content-type: 2.0.0 + content-type: 2.1.0 media-typer: 1.1.0 mime-types: 3.0.2 @@ -40055,26 +39368,26 @@ snapshots: magic-string: 0.30.21 unplugin: 2.3.11 - unctx@3.0.1(magic-string@0.30.21)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))): + unctx@3.0.1(magic-string@0.30.21)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))): optionalDependencies: magic-string: 0.30.21 oxc-parser: 0.143.0 rolldown: 1.2.16 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - unctx@3.0.1(magic-string@1.1.0)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))): + unctx@3.0.1(magic-string@1.1.0)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))): optionalDependencies: magic-string: 1.1.0 oxc-parser: 0.143.0 rolldown: 1.2.16 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - unctx@3.0.1(magic-string@1.1.0)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))): + unctx@3.0.1(magic-string@1.1.0)(oxc-parser@0.152.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))): optionalDependencies: magic-string: 1.1.0 oxc-parser: 0.152.0 rolldown: 1.2.16 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) undefsafe@2.0.5: {} @@ -40140,7 +39453,7 @@ snapshots: dependencies: '@noble/hashes': 2.4.0 - unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.143.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: acorn: 8.18.0 escape-string-regexp: 5.0.0 @@ -40154,7 +39467,7 @@ snapshots: scule: 1.3.0 strip-literal: 4.0.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 optionalDependencies: oxc-parser: 0.143.0 @@ -40169,7 +39482,7 @@ snapshots: - vite - webpack - unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: acorn: 8.18.0 escape-string-regexp: 5.0.0 @@ -40183,7 +39496,7 @@ snapshots: scule: 1.3.0 strip-literal: 4.0.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 optionalDependencies: oxc-parser: 0.152.0 @@ -40198,7 +39511,7 @@ snapshots: - vite - webpack - unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + unimport@6.4.0(esbuild@0.28.1)(oxc-parser@0.152.0)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: acorn: 8.18.0 escape-string-regexp: 5.0.0 @@ -40212,7 +39525,7 @@ snapshots: scule: 1.3.0 strip-literal: 4.0.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) unplugin-utils: 0.3.2 optionalDependencies: oxc-parser: 0.152.0 @@ -40337,7 +39650,7 @@ snapshots: picomatch: 4.0.7 webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.7 @@ -40346,10 +39659,10 @@ snapshots: esbuild: 0.28.1 rolldown: 1.2.16 rollup: 4.62.3 - vite: 6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + vite: 6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) - unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): + unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.7 @@ -40358,8 +39671,8 @@ snapshots: esbuild: 0.28.1 rolldown: 1.2.16 rollup: 4.62.3 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + webpack: 5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3) unstorage@1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.10.1(supports-color@10.0.0)): dependencies: @@ -40401,16 +39714,6 @@ snapshots: mongodb: 3.7.4 ofetch: 1.5.1 - unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@1.5.1): - optionalDependencies: - '@azure/storage-blob': 12.31.0(supports-color@10.0.0) - chokidar: 5.0.0 - db0: 0.3.4(mysql2@3.23.3(@types/node@20.19.43)) - ioredis: 5.11.1(supports-color@10.0.0) - lru-cache: 11.4.0 - mongodb: 7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7) - ofetch: 1.5.1 - unstorage@2.0.0-alpha.7(@azure/storage-blob@12.31.0(supports-color@10.0.0))(chokidar@5.0.0)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0))(lru-cache@11.4.0)(mongodb@7.1.0(@aws-sdk/credential-providers@3.983.0)(socks@2.8.7))(ofetch@2.0.0-alpha.3): optionalDependencies: '@azure/storage-blob': 12.31.0(supports-color@10.0.0) @@ -40541,7 +39844,7 @@ snapshots: '@types/unist': 3.0.0 vfile-message: 4.0.3 - vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0): + vinxi@0.5.11(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(@types/node@20.19.43)(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(debug@4.4.3(supports-color@10.0.0))(encoding@0.1.13)(ioredis@5.11.1(supports-color@10.0.0))(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(sass@1.72.0)(srvx@0.11.22)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))(yaml@2.9.0): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.0.0)) @@ -40562,7 +39865,7 @@ snapshots: hookable: 5.5.3 http-proxy: 1.18.1(debug@4.4.3(supports-color@10.0.0)) micromatch: 4.0.8 - nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + nitropack: 2.13.4(@azure/storage-blob@12.31.0(supports-color@10.0.0))(@parcel/watcher@2.5.6)(encoding@0.1.13)(mysql2@3.23.3(@types/node@20.19.43))(oxc-parser@0.152.0)(rolldown@1.2.16)(srvx@0.11.22)(supports-color@10.0.0)(vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) node-fetch-native: 1.6.7 path-to-regexp: 6.3.0 pathe: 1.1.2 @@ -40575,7 +39878,7 @@ snapshots: unctx: 2.5.0 unenv: 1.10.0 unstorage: 1.17.5(@azure/storage-blob@12.31.0(supports-color@10.0.0))(db0@0.3.4(mysql2@3.23.3(@types/node@20.19.43)))(ioredis@5.11.1(supports-color@10.0.0)) - vite: 6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) zod: 4.6.7 transitivePeerDependencies: - '@azure/app-configuration' @@ -40628,15 +39931,15 @@ snapshots: - xml2js - yaml - vite-dev-rpc@2.0.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vite-dev-rpc@2.0.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: birpc: 4.2.0 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vite-hot-client: 2.2.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite-hot-client: 2.2.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) - vite-hot-client@2.2.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vite-hot-client@2.2.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vite-node@3.2.4(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(supports-color@10.0.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: @@ -40644,7 +39947,7 @@ snapshots: debug: 4.4.3(supports-color@10.0.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -40665,7 +39968,7 @@ snapshots: es-module-lexer: 2.1.0 obug: 2.1.4 pathe: 2.0.3 - vite: 7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -40679,7 +39982,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.14.5(meow@13.2.0)(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(typescript@7.0.2)(vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vite-plugin-checker@0.14.5(meow@13.2.0)(optionator@0.9.4)(oxlint@1.89.0(oxlint-tsgolint@7.0.2004))(typescript@7.0.2)(vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.7 chokidar: 5.0.0 @@ -40688,14 +39991,14 @@ snapshots: picomatch: 4.0.7 proper-lockfile: 4.1.2 tiny-invariant: 1.3.3 - vite: 7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) optionalDependencies: meow: 13.2.0 optionator: 0.9.4 oxlint: 1.89.0(oxlint-tsgolint@7.0.2004) typescript: 7.0.2 - vite-plugin-inspect@11.4.1(@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))))(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vite-plugin-inspect@11.4.1(@nuxt/kit@4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))))(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: ansis: 4.3.1 error-stack-parser-es: 1.0.5 @@ -40705,12 +40008,12 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.2 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vite-dev-rpc: 2.0.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite-dev-rpc: 2.0.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) optionalDependencies: - '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) + '@nuxt/kit': 4.6.4(@nuxt/schema@3.21.11)(confbox@0.2.4)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magic-string@0.30.21)(mlly@1.8.2)(oxc-parser@0.143.0)(rolldown@1.2.16)(unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.16)(rollup@4.62.3)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3))) - vite-plugin-solid@2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vite-plugin-solid@2.11.14(@testing-library/jest-dom@6.9.1)(solid-js@1.9.18)(supports-color@10.0.0)(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7(supports-color@10.0.0) '@types/babel__core': 7.20.5 @@ -40718,27 +40021,27 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.18 solid-refresh: 0.6.3(solid-js@1.9.18)(supports-color@10.0.0) - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vitefu: 1.0.7(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vitefu: 1.0.7(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: - supports-color - vite-plugin-vue-tracer@1.5.0(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)): + vite-plugin-vue-tracer@1.5.0(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0))(vue@3.5.44(typescript@7.0.2)): dependencies: estree-walker: 3.0.3 exsolve: 1.2.0 magic-string: 1.1.0 pathe: 2.0.3 source-map-js: 1.2.2 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vue: 3.5.44(typescript@7.0.2) vite@5.4.21(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2): dependencies: esbuild: 0.21.5 - postcss: 8.5.31 + postcss: 8.5.32 rollup: 4.62.3 optionalDependencies: '@types/node': 20.19.43 @@ -40749,12 +40052,12 @@ snapshots: stylus: 0.59.0(supports-color@10.0.0) terser: 5.51.2 - vite@6.4.3(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): + vite@6.4.5(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - postcss: 8.5.31 + postcss: 8.5.32 rollup: 4.62.3 tinyglobby: 0.2.17 optionalDependencies: @@ -40769,12 +40072,12 @@ snapshots: tsx: 4.23.15 yaml: 2.9.0 - vite@7.3.6(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): + vite@7.3.7(@types/node@20.19.43)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - postcss: 8.5.31 + postcss: 8.5.32 rollup: 4.62.3 tinyglobby: 0.2.17 optionalDependencies: @@ -40789,11 +40092,11 @@ snapshots: tsx: 4.23.15 yaml: 2.9.0 - vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): + vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.31 + postcss: 8.5.32 rolldown: 1.2.16 tinyglobby: 0.2.17 optionalDependencies: @@ -40808,11 +40111,11 @@ snapshots: tsx: 4.23.15 yaml: 2.9.0 - vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): + vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.31 + postcss: 8.5.32 rolldown: 1.2.16 tinyglobby: 0.2.17 optionalDependencies: @@ -40827,27 +40130,27 @@ snapshots: tsx: 4.23.15 yaml: 2.9.0 - vitefu@0.2.5(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vitefu@0.2.5(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): optionalDependencies: - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) vitefu@1.0.7(vite@5.4.21(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)): optionalDependencies: vite: 5.4.21(@types/node@20.19.43)(less@4.2.0(supports-color@10.0.0))(lightningcss@1.33.0)(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2) - vitefu@1.0.7(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): + vitefu@1.0.7(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)): optionalDependencies: - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) - vitest@4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): + vitest@4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@10.0.0))(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 es-module-lexer: 2.1.0 expect-type: 1.4.0 magic-string: 0.30.21 @@ -40859,12 +40162,12 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.10.0 '@types/node': 20.19.43 - '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) + '@vitest/coverage-v8': 4.1.11(vitest@4.1.12) jsdom: 21.1.2(supports-color@10.0.0) transitivePeerDependencies: - '@vitejs/devtools' @@ -40880,15 +40183,15 @@ snapshots: - tsx - yaml - vitest@4.1.11(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@8.1.1))(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): + vitest@4.1.12(@opentelemetry/api@1.10.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.1)(jiti@2.7.0)(jsdom@21.1.2(supports-color@8.1.1))(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@10.0.0))(sass@1.72.0)(stylus@0.59.0(supports-color@10.0.0))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 es-module-lexer: 2.1.0 expect-type: 1.4.0 magic-string: 0.30.21 @@ -40900,12 +40203,12 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.3.5(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) + vite: 8.3.6(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(less@4.2.0(supports-color@8.1.1))(sass@1.72.0)(stylus@0.59.0(supports-color@8.1.1))(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.10.0 '@types/node': 20.19.43 - '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) + '@vitest/coverage-v8': 4.1.11(vitest@4.1.12) jsdom: 21.1.2(supports-color@8.1.1) transitivePeerDependencies: - '@vitejs/devtools' @@ -41034,22 +40337,10 @@ snapshots: walkdir@0.4.1: {} - walker@1.0.7: - dependencies: - makeerror: 1.0.11 - warning@3.0.0: dependencies: loose-envify: 1.4.0 - watch-detector@1.0.2(supports-color@10.0.0): - dependencies: - heimdalljs-logger: 0.1.10(supports-color@10.0.0) - silent-error: 1.1.1(supports-color@10.0.0) - tmp: 0.1.0 - transitivePeerDependencies: - - supports-color - watchpack@2.6.3: dependencies: graceful-fs: 4.2.11 @@ -41078,16 +40369,16 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@5.3.3(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + webpack-dev-middleware@5.3.3(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: colorette: 2.0.20 memfs: 3.6.0 mime-types: 2.1.35 range-parser: 1.2.1 - schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) - webpack-dev-server@4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + webpack-dev-server@4.11.0(debug@4.4.3(supports-color@10.0.0))(supports-color@10.0.0)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -41103,7 +40394,7 @@ snapshots: compression: 1.8.2(supports-color@10.0.0) connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.22.1(supports-color@10.0.0) + express: 4.22.3(supports-color@10.0.0) graceful-fs: 4.2.11 html-entities: 2.5.2 http-proxy-middleware: 2.0.10(@types/express@4.17.21)(debug@4.4.3(supports-color@10.0.0)) @@ -41111,14 +40402,14 @@ snapshots: open: 8.4.0 p-retry: 4.6.2 rimraf: 3.0.2 - schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) selfsigned: 2.4.1 serve-index: 1.9.1(supports-color@10.0.0) sockjs: 0.3.24 spdy: 4.0.2(supports-color@10.0.0) - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) - webpack-dev-middleware: 5.3.3(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) - ws: 8.22.1 + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack-dev-middleware: 5.3.3(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + ws: 8.22.2 transitivePeerDependencies: - bufferutil - debug @@ -41132,16 +40423,16 @@ snapshots: webpack-sources@3.6.0: {} - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)))(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)): dependencies: typed-assert: 1.0.9 - webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) + webpack: 5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3) optionalDependencies: - html-webpack-plugin: 5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) + html-webpack-plugin: 5.6.0(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) webpack-virtual-modules@0.6.2: {} - webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3): + webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -41155,8 +40446,8 @@ snapshots: events: 3.3.0 graceful-fs: 4.2.11 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) - schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) + minimizer-webpack-plugin: 5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@14.18.63))(svgo@4.1.0)(uglify-js@3.13.3)) tapable: 2.3.3 watchpack: 2.6.3 webpack-sources: 3.6.0 @@ -41179,7 +40470,7 @@ snapshots: - uglify-js optional: true - webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3): + webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -41193,8 +40484,8 @@ snapshots: events: 3.3.0 graceful-fs: 4.2.11 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) - schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.31)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + minimizer-webpack-plugin: 5.13.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) + schema-utils: 4.8.1(webpack@5.112.2(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.5.32)(sharp@0.35.5(@types/node@20.19.43))(svgo@4.1.0)(uglify-js@3.13.3)) tapable: 2.3.3 watchpack: 2.6.3 webpack-sources: 3.6.0 @@ -41216,7 +40507,7 @@ snapshots: - svgo - uglify-js - webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3): + webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 0.0.51 @@ -41239,7 +40530,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.3.3 - terser-webpack-plugin: 5.6.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.31))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) + terser-webpack-plugin: 5.6.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)(webpack@5.76.1(clean-css@5.2.2)(cssnano@7.1.9(postcss@8.5.32))(csso@5.0.5)(esbuild@0.15.5)(html-minifier-terser@6.1.0)(lightningcss@1.33.0)(postcss@8.4.31)(uglify-js@3.13.3)) watchpack: 2.6.3 webpack-sources: 3.6.0 transitivePeerDependencies: @@ -41343,11 +40634,11 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 - which@1.3.1: + which@2.0.2: dependencies: isexe: 2.0.0 - which@2.0.2: + which@3.0.1: dependencies: isexe: 2.0.0 @@ -41418,24 +40709,6 @@ snapshots: workerpool@9.3.4: {} - wrangler@4.156.0(@cloudflare/workers-types@4.20250922.0)(@types/node@20.19.43): - dependencies: - '@cloudflare/kv-asset-handler': 0.5.0 - '@cloudflare/unenv-preset': 2.16.2(unenv@2.0.0-rc.24)(workerd@1.20261023.1) - blake3-wasm: 2.1.5 - esbuild: 0.28.2 - miniflare: 5.20261023.3-beta(@types/node@20.19.43) - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20261023.1 - optionalDependencies: - '@cloudflare/workers-types': 4.20250922.0 - fsevents: 2.3.3 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - utf-8-validate - wrangler@4.156.0(@cloudflare/workers-types@4.20260519.1)(@types/node@20.19.43): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 @@ -41501,7 +40774,7 @@ snapshots: ws@8.21.0: {} - ws@8.22.1: {} + ws@8.22.2: {} wsl-utils@0.3.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 42cc03476a2d..75d66d27027b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -121,8 +121,9 @@ allowBuilds: workerd: true minimumReleaseAgeExclude: - '@opentelemetry/api@1.10.0' - - '@sentry/conventions@0.30.0' - - '@sentry/rrdom@2.45.0' - - '@sentry/rrweb-snapshot@2.45.0' - - '@sentry/rrweb-types@2.45.0' - - '@sentry/rrweb@2.45.0' + - '@sentry/conventions@0.32.0' + - '@sentry/rrdom@2.46.0' + - '@sentry/rrweb-snapshot@2.46.0' + - '@sentry/rrweb-types@2.46.0' + - '@sentry/rrweb@2.46.0' + - sentry@0.49.0 diff --git a/scripts/rm.mjs b/scripts/rm.mjs new file mode 100644 index 000000000000..b35be8d32371 --- /dev/null +++ b/scripts/rm.mjs @@ -0,0 +1,5 @@ +import { rmSync } from 'node:fs'; + +for (const path of process.argv.slice(2)) { + rmSync(path, { recursive: true, force: true }); +}