From 22ae7bbae29735c92581119a995a6851a19b31f4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 11:46:05 +0100 Subject: [PATCH 01/92] chore(deps): Bump follow-redirects from 1.16.0 to 1.16.2 (#26117) **Why:** Fixes the open follow-redirects advisories (3 high). Lockfile only, since the parents use `^` ranges. The remaining 1.16.0 copy comes from `axios@1.18.1` in nx 22 and goes away with #26110. Dependabot alert: https://github.com/getsentry/sentry-javascript/security/dependabot/3402 Co-authored-by: Claude Opus 5.5 --- pnpm-lock.yaml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 753171def2ad..b037e4383ec0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12374,6 +12374,15 @@ packages: debug: optional: true + follow-redirects@1.16.2: + resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -29359,7 +29368,7 @@ snapshots: 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 @@ -32354,14 +32363,14 @@ snapshots: fn.name@1.1.0: {} - follow-redirects@1.16.0(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) + follow-redirects@1.16.2(debug@4.4.3(supports-color@10.0.0)): + optionalDependencies: + debug: 4.4.3(supports-color@10.0.0) + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -33204,7 +33213,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 @@ -35144,7 +35153,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) From b691a573df64c677f69a524783c532e34ace4d46 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 11:56:51 +0100 Subject: [PATCH 02/92] chore(deps): Bump @modelcontextprotocol/server and client to 2.4.0 (#26116) **Why:** Fixes the open `@modelcontextprotocol/server` advisories (3 high, 1 medium) by bumping our node-integration-tests devDependencies to 2.4.0. The MCP suites pass locally. Dependabot alert: https://github.com/getsentry/sentry-javascript/security/dependabot/3385 **Unsure about:** `@flue/runtime@2.3.0` (latest) pins `@modelcontextprotocol/client` to exactly 2.0.0, so the 2 client alerts stay open until flue updates. Co-authored-by: Claude Opus 5.5 --- .../node-integration-tests/package.json | 4 +- pnpm-lock.yaml | 38 +++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index c6413087ad1a..b4013ef28e90 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -51,9 +51,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b037e4383ec0..5fd8ba9e04c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -670,14 +670,14 @@ importers: 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 @@ -6076,10 +6076,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 +6098,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': @@ -24989,10 +24997,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) @@ -25017,9 +25039,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': From a2dbec000b9e91672d579fae408e1de01ee68a15 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 11:57:05 +0100 Subject: [PATCH 03/92] chore(deps): Bump vite to 6.4.5 and 7.3.7 (#26113) **Why:** Fixes the open vite 6.x and 7.x advisories. Both copies are transitive and come in through `^` ranges, so this only changes the lockfile. `vite@5.4.21` stays: it only comes from the `astro@4` devDependency baseline, and there is no patched 5.x. Dependabot alert: https://github.com/getsentry/sentry-javascript/security/dependabot/3340 Co-authored-by: Claude Opus 5.5 --- pnpm-lock.yaml | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fd8ba9e04c5..b4b5429c8a7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18823,8 +18823,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: @@ -18863,8 +18863,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: @@ -25641,8 +25641,8 @@ snapshots: 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)) + '@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.31) consola: 3.4.2 cssnano: 7.1.9(postcss@8.5.31) @@ -25668,9 +25668,9 @@ snapshots: 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: @@ -28428,22 +28428,22 @@ snapshots: 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) - '@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)': @@ -35657,7 +35657,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.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)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.62.3) @@ -35722,7 +35722,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.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-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 @@ -40200,7 +40200,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.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)): dependencies: acorn: 8.18.0 escape-string-regexp: 5.0.0 @@ -40214,7 +40214,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.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-utils: 0.3.2 optionalDependencies: oxc-parser: 0.152.0 @@ -40368,7 +40368,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.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)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.7 @@ -40377,7 +40377,7 @@ 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) + 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.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)): @@ -40593,7 +40593,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.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-fetch-native: 1.6.7 path-to-regexp: 6.3.0 pathe: 1.1.2 @@ -40606,7 +40606,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' @@ -40675,7 +40675,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 @@ -40696,7 +40696,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 @@ -40710,7 +40710,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 @@ -40719,7 +40719,7 @@ 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 @@ -40780,7 +40780,7 @@ 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) @@ -40800,7 +40800,7 @@ 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) From c054e7a1422c6a9a3a8d089ae2b27dc825c39853 Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Thu, 5 Nov 2026 13:59:39 +0100 Subject: [PATCH 04/92] fix(server-utils): Mark AI streaming spans as errored when the stream fails midway (#26023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a streamed OpenAI, Anthropic or Google GenAI response threw partway through, for example on a dropped connection, the `gen_ai` span still ended `ok`. The stream wrappers had `try/finally` with no `catch`. They now set `internal_error`, as Workers AI does. Google sets `cancelled` when the caller's `abortSignal` fired, while its timeouts stay errors. `messages.stream()` failures now keep partial usage too. A consumer that stops with `break` keeps the span `ok`. One that throws into the generator, which `Readable.from` does when a piped client disconnects, records `internal_error`. Fixes #25994 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../suites/tracing/anthropic/test.ts | 13 +- .../scenario-stream-midway-error.mjs | 103 +++++++++++++ .../suites/tracing/google-genai/test.ts | 35 +++++ .../openai/scenario-stream-midway-error.mjs | 81 ++++++++++ .../suites/tracing/openai/test.ts | 30 ++++ .../src/ai/anthropic-ai/streaming.ts | 5 +- .../server-utils/src/ai/google-genai/index.ts | 7 +- .../src/ai/google-genai/streaming.ts | 4 + .../server-utils/src/ai/openai/streaming.ts | 3 + .../src/integrations/google-genai.ts | 17 +- .../test/ai/lib/tracing/stream-errors.test.ts | 145 ++++++++++++++++++ 11 files changed, 434 insertions(+), 9 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-stream-midway-error.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/openai/scenario-stream-midway-error.mjs create mode 100644 packages/server-utils/test/ai/lib/tracing/stream-errors.test.ts 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/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/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/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/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/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/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/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' }); + }); +}); From 55d19671b9f0351903d95cd33fe9f59bbb8e5df4 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:32:29 +0100 Subject: [PATCH 05/92] fix(server-utils): Keep Bedrock InvokeModel spans for byte, non-JSON, or model-less bodies (#26124) ## What Bedrock `InvokeModel` and `InvokeModelWithResponseStream` calls now always get a span. - A request body passed as bytes is read. - A request body that is not JSON, or a request without a `modelId`, no longer drops the span. ## Why Parsing the request body threw before the span started, so these calls had no span at all. Extracted from #26118. Co-authored-by: Claude Opus 5.5 --- .../aws-sdk/services/bedrock-runtime.ts | 17 ++++- .../test/integrations/aws-sdk-bedrock.test.ts | 73 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts 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..5dfe2b0f55b6 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 @@ -137,8 +137,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; @@ -412,6 +412,19 @@ function setUsage(span: Span, usage: TokenUsage | undefined): void { } } +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; + } +} + function parseChunk(bytes?: Uint8Array): ParsedChunk { if (!bytes || !(bytes instanceof Uint8Array)) { return null; 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..3cbcc70e0db1 --- /dev/null +++ b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts @@ -0,0 +1,73 @@ +import { + GEN_AI_OPERATION_NAME, + GEN_AI_PROVIDER_NAME, + GEN_AI_REQUEST_MAX_TOKENS, + GEN_AI_REQUEST_MODEL, + GEN_AI_REQUEST_TEMPERATURE, +} from '@sentry/conventions/attributes'; +import { GEN_AI_GENERATE_CONTENT } from '@sentry/conventions/op'; +import { describe, expect, it } from 'vitest'; +import { BedrockRuntimeServiceExtension } from '../../src/integrations/aws-sdk/services/bedrock-runtime'; +import type { RequestMetadata } from '../../src/integrations/aws-sdk/types'; + +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', + }, + }); + }); +}); From f5e56406eb3b8b4fd97f1fa0f10108c7e4fda869 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 14:48:35 +0100 Subject: [PATCH 06/92] fix(esbuild): Avoid import-is-undefined warning for entries without a default export (#26132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** The debug ID proxy statically re-exports the entry's default export so downstream bundlers can tree-shake, which makes esbuild warn with `import-is-undefined` whenever an entry has no default export. esbuild can't suppress a diagnostic per file, so the plugin sets `logOverride['import-is-undefined'] = 'silent'` in `initialOptions`, same as it already forces `metafile`. The proxy's path, suffix and namespace stay as they are, so other plugins, source maps and the metafile are unaffected. Tradeoff: this silences the warning for the whole build. A user-provided `logOverride` for that id wins, so it can be turned back on. Supersedes #26123, which relied on an undocumented esbuild heuristic and changed the proxy's namespace and path in ways other plugins observe. **How to review:** Four lines in `packages/bundler-plugins/src/esbuild/index.ts`, plus `test/esbuild/entry-without-default-export.test.ts`. Closes #25885 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 --- packages/bundler-plugins/src/esbuild/index.ts | 5 ++ .../entry-without-default-export.test.ts | 63 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 packages/bundler-plugins/test/esbuild/entry-without-default-export.test.ts 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/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/, + ); + }); +}); From 89d33a99766adcc519d8b76b6f895a5aafa22af2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 15:07:43 +0100 Subject: [PATCH 07/92] fix(sveltekit): Read navigation untracked in the Kit 3 fetch wrapper (#26136) **Why:** On SvelteKit 3 the fetch wrapper reads `navigating` from `$app/state` before every request. When a `$effect` calls `fetch` synchronously, that read made `navigating` a dependency of the effect, so it re-ran (and re-sent its requests) on every navigation. The read now goes through `untrack`. `svelte` is marked external in rollup so the import resolves to the app's Svelte 5, and a small build-time `.d.ts` declares `untrack` because the package is type-checked against Svelte 4. **How to review:** The fix is one line in `navigationState.svelte.ts`. The new e2e test in `sveltekit-3` fails without it (4 requests instead of 1). Closes #26129 Co-authored-by: Claude Opus 5.5 --- .../src/routes/effect-fetch/+page.svelte | 13 ++++++++++++ .../sveltekit-3/tests/tracing.client.test.ts | 20 +++++++++++++++++++ packages/sveltekit/rollup.npm.config.mjs | 8 +++++++- .../src/client/navigationState.svelte.ts | 5 ++++- packages/sveltekit/src/client/svelte5.d.ts | 7 +++++++ 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/effect-fetch/+page.svelte create mode 100644 packages/sveltekit/src/client/svelte5.d.ts 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/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; +} From cd73114aa9108cbf98ca2ed265150ea8f57438e6 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:10:10 +0100 Subject: [PATCH 08/92] fix(server-utils): End Bedrock stream spans with an error status on failure (#26131) ## What Bedrock streaming spans now show failures and always end. - A `ConverseStream` or `InvokeModelWithResponseStream` stream that fails sets the span status to error, or to cancelled when the caller aborted it. - A streaming response without a stream, or without a `modelId`, no longer leaves the span open. ## Why Failed streams showed as successful, and some streaming spans were never sent. Extracted from #26118. --------- Co-authored-by: Claude Opus 5.5 --- .../src/integrations/aws-sdk/index.ts | 1 + .../aws-sdk/services/bedrock-runtime.ts | 26 +++++- .../src/integrations/aws-sdk/types.ts | 2 + .../test/integrations/aws-sdk-bedrock.test.ts | 89 ++++++++++++++++++- 4 files changed, 112 insertions(+), 6 deletions(-) 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 5dfe2b0f55b6..295c0201301e 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, @@ -258,9 +258,14 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { } 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 { @@ -338,13 +343,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 +366,9 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { } yield chunk; } + } catch (error) { + setStreamErrorStatus(span, abortSignal); + throw error; } finally { span.end(); } @@ -380,6 +390,7 @@ function resolveStreamRecorder(modelId: string): ((parsedChunk: ParsedChunk, spa async function* wrapConverseStreamResponse( stream: AsyncIterable, span: Span, + abortSignal: AbortSignal | undefined, ): AsyncGenerator { try { let usage: TokenUsage | undefined; @@ -389,11 +400,18 @@ async function* wrapConverseStreamResponse( yield item; } setUsage(span, usage); + } 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]); 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/test/integrations/aws-sdk-bedrock.test.ts b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts index 3cbcc70e0db1..827bd1b60ff0 100644 --- a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts +++ b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts @@ -6,9 +6,12 @@ import { GEN_AI_REQUEST_TEMPERATURE, } from '@sentry/conventions/attributes'; import { GEN_AI_GENERATE_CONTENT } from '@sentry/conventions/op'; -import { describe, expect, it } from 'vitest'; +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 { RequestMetadata } from '../../src/integrations/aws-sdk/types'; +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'; @@ -71,3 +74,85 @@ describe('BedrockRuntimeServiceExtension InvokeModel request', () => { }); }); }); + +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('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(); + }); +}); From ccb607617b108343ae536fa22a5c8acb182ceb12 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 15:27:15 +0100 Subject: [PATCH 09/92] chore(deps): Bump nx from 22.7.10 to 24.0.3 (#26110) **Why:** nx 22 pins `axios@1.18.1`, which has open advisories, and every 22.x release does. nx 24 drops axios. We skip nx 23 because it pins `undici@7.29.0`, which is still vulnerable, while 24.0.3 pins the patched 7.30.1. 24.0.4 is too new for our `minimumReleaseAge`. Dependabot alert: https://github.com/getsentry/sentry-javascript/security/dependabot/3311 **How to review:** The only config change is `affected.legacy: true`, added by the official nx 24 migration so `nx affected` (`test:pr`) keeps selecting whole projects instead of individual tasks. The other nx 23/24 migrations made no changes. **Unsure about:** nx 23 dropped Node 20 support, but our `Node (20.19) Unit Tests` job still runs nx on Node 20 (through `test:pr:node`). It passes today and nx declares no `engines`, so nothing blocks it, but a future nx release could break that job. If that happens, the job would need to run nx on the repo's Node version and only the tests on Node 20. Co-authored-by: Claude Opus 5.5 --- nx.json | 5 +- package.json | 2 +- pnpm-lock.yaml | 290 ++++++++++++++++++------------------------------- 3 files changed, 111 insertions(+), 186 deletions(-) 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..2e35ff61e95b 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e113bbc5441b..9d6be35145ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -4160,10 +4160,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'} @@ -6396,57 +6404,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] @@ -9933,9 +9941,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==} @@ -10189,10 +10194,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'} @@ -11511,10 +11512,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==} @@ -12225,6 +12222,9 @@ packages: fast-uri@4.2.1: resolution: {integrity: sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==} + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -12355,10 +12355,6 @@ packages: 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==} @@ -12373,15 +12369,6 @@ 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==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - follow-redirects@1.16.2: resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} engines: {node: '>=4.0'} @@ -13055,12 +13042,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: @@ -13647,9 +13634,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==} @@ -15137,8 +15121,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 @@ -15348,10 +15332,6 @@ packages: 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'} @@ -17132,11 +17112,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'} @@ -17344,8 +17319,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: @@ -19286,6 +19261,11 @@ packages: 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} @@ -23099,11 +23079,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 @@ -25707,34 +25699,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': {} @@ -28915,12 +28907,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) @@ -29378,16 +29364,6 @@ 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.2(debug@4.4.3(supports-color@10.0.0)) @@ -29734,10 +29710,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 @@ -31294,10 +31266,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: {} @@ -32182,6 +32150,10 @@ snapshots: fast-uri@4.2.1: {} + fast-wrap-ansi@0.2.0: + dependencies: + fast-string-width: 3.0.2 + fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 @@ -32375,8 +32347,6 @@ snapshots: flatted: 3.4.2 rimraf: 3.0.2 - flat@5.0.2: {} - flatted@3.4.2: {} flattie@1.1.1: {} @@ -32385,10 +32355,6 @@ snapshots: fn.name@1.1.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) - follow-redirects@1.16.2(debug@4.4.3(supports-color@10.0.0)): optionalDependencies: debug: 4.4.3(supports-color@10.0.0) @@ -33249,13 +33215,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) @@ -33331,9 +33290,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: {} @@ -33981,8 +33940,6 @@ snapshots: jsonc-parser@3.1.0: {} - jsonc-parser@3.2.0: {} - jsonc-parser@3.3.1: {} jsondiffpatch@0.6.0: @@ -35008,7 +34965,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.12 minimatch@11.0.3: dependencies: @@ -36224,8 +36181,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 @@ -36234,19 +36193,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 @@ -36254,74 +36208,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 @@ -36329,11 +36259,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 @@ -36341,16 +36272,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: @@ -36537,17 +36468,6 @@ snapshots: 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 @@ -38797,8 +38717,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): @@ -39156,7 +39074,7 @@ snapshots: smob@1.4.1: {} - smol-toml@1.6.1: {} + smol-toml@1.9.0: {} sockjs@0.3.24: dependencies: @@ -41382,6 +41300,10 @@ snapshots: dependencies: isexe: 2.0.0 + which@3.0.1: + dependencies: + isexe: 2.0.0 + which@4.0.0: dependencies: isexe: 3.1.1 From eb0e92e9a3b88e8ca4dc9e65ddfdf5215817caf9 Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Thu, 5 Nov 2026 16:04:08 +0100 Subject: [PATCH 10/92] fix(server-utils): Create an invoke_agent span for LangGraph stream() (#25749) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CompiledGraph.stream()` produced no `gen_ai.invoke_agent` span, so the chat and tool spans of a streamed run had no agent span above them. LangGraph runs the graph as soon as `stream()` is called, whether or not anyone reads it, so the span now follows the run. It ends on the run's own `handleChainEnd`/`handleChainError`, and the output comes from the final state, the same way `invoke()` records it. `invoke()` keeps its single span. The shared setup also no longer writes Sentry's metadata and callbacks into the caller's config, and a graph seen by both `instrumentStateGraph()` and the integration is wrapped once, with the manual options winning. **Known limitation.** When a caller stops reading early, LangGraph keeps running the graph but never reports the run's end. After a `break` the span ends at the break, so later nodes' spans start after it. After `reader.cancel()` or a client disconnecting from `new Response(stream)`, the span never ends. Replaces #23615. Co-authored with @psh4607 from that PR. Fixes #19626 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Seongho.Bak Co-authored-by: Claude Opus 5.5 --- .../suites/tracing/langgraph/index.ts | 6 +- .../suites/tracing/langgraph/test.ts | 21 +- .../langgraph/agent-stream-scenario.mjs | 53 +++ .../tracing/langgraph/scenario-stream.mjs | 90 ++++ .../suites/tracing/langgraph/test.ts | 86 ++++ .../tracing/langgraph/v1/scenario-stream.mjs | 102 +++++ .../suites/tracing/langgraph/v1/test.ts | 65 ++- .../server-utils/src/ai/langchain/utils.ts | 29 +- .../src/ai/langgraph/agent-span.ts | 144 +++++++ .../server-utils/src/ai/langgraph/index.ts | 264 +++++------- .../server-utils/src/ai/langgraph/stream.ts | 140 +++++++ .../server-utils/src/ai/langgraph/types.ts | 1 + .../src/integrations/langgraph.ts | 52 +-- .../ai/lib/tracing/langchain-utils.test.ts | 29 ++ .../ai/lib/tracing/langgraph-stream.test.ts | 387 ++++++++++++++++++ 15 files changed, 1244 insertions(+), 225 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/agent-stream-scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/scenario-stream.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/langgraph/v1/scenario-stream.mjs create mode 100644 packages/server-utils/src/ai/langgraph/agent-span.ts create mode 100644 packages/server-utils/src/ai/langgraph/stream.ts create mode 100644 packages/server-utils/test/ai/lib/tracing/langgraph-stream.test.ts 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/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..eeb6772cc01c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -7,6 +7,7 @@ import { GEN_AI_OUTPUT_MESSAGES, GEN_AI_PIPELINE_NAME, GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_SYSTEM_INSTRUCTIONS, @@ -18,6 +19,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'; @@ -365,4 +367,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/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/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/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'); + }); +}); From e64d7982ea3bb9b6da94c04904dfa0ab9c52bfb1 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Thu, 5 Nov 2026 20:32:10 +0500 Subject: [PATCH 11/92] feat(server-utils): Always warn on Bun when no channel injection is detected (#25857) **Why:** Under `bun run` there is no `Module.registerHooks`, so the runtime injection never registers and the channel integrations stay silent, including their error capture. The only hint was a `debug.warn`, so on Bun this is now an always-on `consoleSandbox` warning that points to `@sentry/bun/plugin`. Node keeps the debug-only message. Closes #23882 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: isaacs --- .../scenario-no-integrations.ts | 6 ++ .../suites/no-channel-injection/scenario.ts | 6 ++ .../suites/no-channel-injection/test.ts | 22 +++++++ .../utils/runner/createRunner.ts | 12 +++- packages/core/src/utils/worldwide.ts | 10 +++ .../server-utils/src/orchestrion/detect.ts | 20 +++++- .../src/orchestrion/instrumentation.ts | 2 + .../test/orchestrion/detect.test.ts | 66 ++++++++++++++++++- .../test/orchestrion/instrumentation.test.ts | 6 ++ 9 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 dev-packages/bun-integration-tests/suites/no-channel-injection/scenario-no-integrations.ts create mode 100644 dev-packages/bun-integration-tests/suites/no-channel-injection/scenario.ts create mode 100644 dev-packages/bun-integration-tests/suites/no-channel-injection/test.ts 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/node-integration-tests/utils/runner/createRunner.ts b/dev-packages/node-integration-tests/utils/runner/createRunner.ts index b592869e9aca..db07a127ff12 100644 --- a/dev-packages/node-integration-tests/utils/runner/createRunner.ts +++ b/dev-packages/node-integration-tests/utils/runner/createRunner.ts @@ -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}'`)); } }); 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/server-utils/src/orchestrion/detect.ts b/packages/server-utils/src/orchestrion/detect.ts index 572afea7b5f5..c0674371e50a 100644 --- a/packages/server-utils/src/orchestrion/detect.ts +++ b/packages/server-utils/src/orchestrion/detect.ts @@ -1,4 +1,4 @@ -import { debug, GLOBAL_OBJ } from '@sentry/core'; +import { consoleSandbox, debug, GLOBAL_OBJ } from '@sentry/core'; /** * The module names (e.g. `mysql`, `@hapi/hapi`) orchestrion has already injected @@ -32,9 +32,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 && typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') { + 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..bf793330c9eb 100644 --- a/packages/server-utils/src/orchestrion/instrumentation.ts +++ b/packages/server-utils/src/orchestrion/instrumentation.ts @@ -58,6 +58,8 @@ export function invokeOrchestrionInstrumentation { 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(); From da4348d1a3e7eef11d9e005d1b58b7b151dae945 Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:34:56 +0100 Subject: [PATCH 12/92] chore: Add external contributor to CHANGELOG.md (#26152) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #25857 Co-authored-by: isaacs <9287+isaacs@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3624e80c8a..822fc26dcded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott +Work in this release was contributed by @lazerg. Thank you for your contribution! + ## 11.11.0 ### Important Changes From c7d2764952a3b7885ee160f82c1339e0b36253f6 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Nov 2026 16:53:27 +0100 Subject: [PATCH 13/92] fix(node): Patch the mutable fs object in fsIntegration under native ESM (#26086) **Why:** In native ESM, `fsIntegration` crashed with `Cannot assign to read only property`, because it patched the read-only `import * as fs` namespace. It now patches the mutable `fs` object behind the default export. A plain `import fs from 'fs'` isn't an option: our rollup `interop: 'esModule'` setting would turn it into `require('fs').default` in the CJS build, which is undefined. After patching, it calls `syncBuiltinESMExports()` so named imports (`import { readFile } from 'fs'`) pick up the patched functions. That call is wrapped in try/catch because Cloudflare workerd exports it but throws when it's called. **How to review:** The fix is in `instrumentation.ts`. The fs suite now runs in both ESM and CJS. On Bun and Deno, the ESM `node:fs` exports aren't tied to the object we patch, so only `fs.promises` calls get spans there. For that reason those ESM tests run on Node only. The named-import test is ESM-only, because CJS destructuring copies the functions before the patch is applied. **Unsure about:** `syncBuiltinESMExports()` updates the ESM exports of every builtin module, not only `fs`. It only makes patches applied to the CJS objects visible to ESM importers, so it shouldn't affect anything else. Closes #25950 --------- Co-authored-by: Claude Opus 5.5 --- .../suites/fs-instrumentation/scenario.mjs | 90 ++++----- .../suites/fs-instrumentation/test.ts | 172 ++++++++++++------ .../fs/vendored/instrumentation.ts | 14 +- 3 files changed, 176 insertions(+), 100 deletions(-) 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/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts index d341a3d9ffa3..a6cbf2aef79c 100644 --- a/packages/node/src/integrations/fs/vendored/instrumentation.ts +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -25,12 +25,17 @@ import { suppressTracing, withActiveSpan, } from '@sentry/core'; -import * as fs from 'fs'; +import * as fsNamespace from 'fs'; +import { syncBuiltinESMExports } 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; + const SPAN_ORIGIN = 'auto.file.fs'; const SPAN_OP = FILE; @@ -304,6 +309,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. + } } /** From 9d0f9c36eb35abe86439b1af319bc8655cff04b9 Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Thu, 5 Nov 2026 20:15:11 +0100 Subject: [PATCH 14/92] feat(server-utils): Group the tool spans of an Anthropic session runner under one invoke_agent span (#25995) --- .../v0.129/scenario-environment-worker.mjs | 108 +++++ .../suites/tracing/anthropic/v0.129/test.ts | 70 +++- .../suites/tracing/anthropic/v1/test.ts | 21 +- packages/core/src/utils/object.ts | 8 +- packages/core/test/lib/utils/object.test.ts | 19 + .../src/ai/anthropic-ai/tool-runner.ts | 91 ++++- .../tracing/anthropic-ai-tool-runner.test.ts | 384 +++++++++++++++++- 7 files changed, 672 insertions(+), 29 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/anthropic/v0.129/scenario-environment-worker.mjs 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/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/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/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/test/ai/lib/tracing/anthropic-ai-tool-runner.test.ts b/packages/server-utils/test/ai/lib/tracing/anthropic-ai-tool-runner.test.ts index f27b5e85a62d..5736a661188c 100644 --- a/packages/server-utils/test/ai/lib/tracing/anthropic-ai-tool-runner.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/anthropic-ai-tool-runner.test.ts @@ -1,15 +1,35 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ErrorEvent, Span } from '@sentry/core'; -import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON, startSpan } from '@sentry/core'; import { + continueTrace, + conversationIdIntegration, + getClient, + getCurrentScope, + getIsolationScope, + getMainCarrier, + getTraceData, + setCurrentClient, + spanToStaticSpanJSON, + SentryNonRecordingSpan, + setConversationId, + startSpan, + withActiveSpan, + withIsolationScope, + 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, } 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 { instrumentAnthropicAiClient } from '../../../../src/ai/anthropic-ai'; +import { instrumentSessionToolRunner } from '../../../../src/ai/anthropic-ai/tool-runner'; +import { setAsyncLocalStorageAsyncContextStrategy } from '../../../../src/async-context'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../src/ai/core/gen-ai-attributes'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; @@ -32,11 +52,21 @@ class FakeRunner { } } -// Mirrors the SDK session runner: it keeps `opts.tools` as `tools` and passes the call event as both `toolUse` and `toolUseBlock`. +type ToolCall = { name: string; id: string; input: unknown }; + +// Mirrors the SDK session runner: it keeps `opts.tools` as `tools`, passes the call event as both `toolUse` and +// `toolUseBlock`, and starts the background loop that runs the tools on the first `next()`. class FakeSessionRunner { public readonly tools: Tool[]; - public constructor(opts: { tools: Tool[] }) { - this.tools = opts.tools; + public drained = false; + private readonly _calls: ToolCall[]; + private _consumed = false; + public constructor( + public readonly sessionId: string, + { tools, calls = [] }: { tools: Tool[]; calls?: ToolCall[] }, + ) { + this.tools = tools; + this._calls = calls; } public async dispatch(name: string, toolUse: { id: string; input: unknown }): Promise<{ isError: boolean }> { 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'); From f547f64bc797e76514d73f2e184bfbf8f53effa1 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 5 Nov 2026 14:59:30 -0500 Subject: [PATCH 15/92] fix(node): Read syncBuiltinESMExports off the module namespace so Deno can load @sentry/node (#26155) Importing `@sentry/node` under Deno before 2.8.2 would fail due to #26086 named-imported `syncBuiltinESMExports`, which those versions only expose on the default export of `node:module`, fixed by reading it off the module namespace instead. I found this while running an exports check on different runtimes, it is very low impact and severity as I don't expect deno users to run the Node SDK stuff there, but this didn't break before either. Feel free to close if too edgy. --- .../integrations/fs/vendored/instrumentation.ts | 9 +++++++-- packages/node/test/integrations/fs.test.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 packages/node/test/integrations/fs.test.ts diff --git a/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts index a6cbf2aef79c..b878fff7f79d 100644 --- a/packages/node/src/integrations/fs/vendored/instrumentation.ts +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -26,7 +26,7 @@ import { withActiveSpan, } from '@sentry/core'; import * as fsNamespace from 'fs'; -import { syncBuiltinESMExports } from 'module'; +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'; @@ -36,6 +36,11 @@ import { indexFs } from './utils'; // 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; @@ -312,7 +317,7 @@ export function enableFsInstrumentation(config: FsInstrumentationConfig = {}): v // Named ESM imports (`import { readFile } from 'fs'`) are snapshots of the CJS exports until synced. try { - syncBuiltinESMExports(); + syncBuiltinESMExports?.(); } catch { // Cloudflare workerd exports `syncBuiltinESMExports` but throws `ERR_METHOD_NOT_IMPLEMENTED` when it is called. } 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); + }); +}); From 16e3c0ab5d29e001cbd7de219a3466c5d7430084 Mon Sep 17 00:00:00 2001 From: Michi Hoffmann Date: Fri, 6 Nov 2026 06:20:12 +0100 Subject: [PATCH 16/92] docs: Point Sentry Conventions links to conventions.sentry.dev (#26156) Migrate to https://conventions.sentry.dev/ Co-authored-by: Claude Opus 5.5 --- .agents/skills/add-ai-integration/SKILL.md | 2 +- .agents/skills/port-span-names/SKILL.md | 6 +- MIGRATION.md | 2 +- packages/core/src/integrations/supabase.ts | 2 +- packages/core/src/tracing/spans/spanNames.ts | 66 +++++++++---------- packages/core/src/utils/request.ts | 4 +- .../hooks/updateRouteBeforeResponse.ts | 2 +- packages/server-utils/src/ai/mastra/utils.ts | 2 +- .../nitro/captureTracingEvents.ts | 2 +- 9 files changed, 44 insertions(+), 44 deletions(-) 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/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/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/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/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/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/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/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), From 4c9a4eb3a3becea4f6a623d3b06686bb6c28ff86 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 09:32:52 +0100 Subject: [PATCH 17/92] test(node): collect TypeSafe spans across streamed envelopes (#26151) **Why:** TypeSafe integration tests assume all four decide spans arrive in one streamed container. Collecting across envelopes removes that batching assumption while preserving every span assertion, including waiting for late children after the `main` segment. Not sure if this actually fixes it but worth a shot I have seen this flake couple of times. Co-authored-by: OpenAI Codex --- .../suites/tracing/typesafe/test.ts | 192 +++++++++--------- 1 file changed, 97 insertions(+), 95 deletions(-) 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/); + } }); }); }); From 8c2bc971483d33e91b929477f8e0648e3d20918d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 6 Nov 2026 11:21:34 +0100 Subject: [PATCH 18/92] chore(deps): Bump body-parser to fix CVE-2026-111181 (#26164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Fixes Dependabot alerts [3366](https://github.com/getsentry/sentry-javascript/security/dependabot/3366), [3365](https://github.com/getsentry/sentry-javascript/security/dependabot/3365) and [3250](https://github.com/getsentry/sentry-javascript/security/dependabot/3250) (body-parser DoS, high). Bumps body-parser 1.20.4 β†’ 1.20.9 and 2.3.0 β†’ 2.4.0. All parents (express 4/5, @apollo/server) already allow these versions, so it is a lockfile refresh plus the direct `^2.4.0` in node-integration-tests. Dev/test-only. It also pulls in newer qs and content-type through body-parser. Co-authored-by: Claude Opus 5.5 --- .../node-integration-tests/package.json | 2 +- pnpm-lock.yaml | 53 +++++++++++-------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 1542e2e3e2fd..e3e8bc7beeb0 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -74,7 +74,7 @@ "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d6be35145ca..de50736bca0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -739,8 +739,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) @@ -10160,12 +10160,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: @@ -10761,8 +10761,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: @@ -16411,8 +16411,12 @@ packages: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.2: + resolution: {integrity: sha512-VYdnB5S7/0IeXzBacUtwNAaM6d7Y/PgYnNC7bJCI6meqA/OgnsQbZMtVW8Dt2D1PHgbHGtYmhSoEIgvt7xZ2ZA==} + engines: {node: '>=0.6'} + + qs@6.17.1: + resolution: {integrity: sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -19953,7 +19957,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) @@ -29644,7 +29648,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 @@ -29654,22 +29658,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: @@ -30414,7 +30418,7 @@ snapshots: content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@1.7.0: dependencies: @@ -31984,7 +31988,7 @@ snapshots: 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 @@ -32019,7 +32023,7 @@ snapshots: express@5.2.1(supports-color@10.0.0): dependencies: accepts: 2.0.0 - body-parser: 2.3.0(supports-color@10.0.0) + body-parser: 2.4.0(supports-color@10.0.0) content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 @@ -32038,7 +32042,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) @@ -37835,7 +37839,12 @@ snapshots: dependencies: side-channel: 1.1.1 - qs@6.15.3: + qs@6.16.2: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + qs@6.17.1: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -39891,7 +39900,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 From 587f05a33c4f5d888b2329f08ba7d0685fc64b4e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 6 Nov 2026 11:50:33 +0100 Subject: [PATCH 19/92] fix(core): Avoid stack overflow in FunctionToString on cyclic Proxy prototype chains (#26162) **Why:** The patched `toString` looked up `__sentry_original__` through the prototype chain. If that chain loops through a Proxy, the lookup overflows the stack, while native `toString` works fine. We now only read it when it's an own property, which is where `markFunctionWrapped` puts it. The Hono proxies only exposed it through a `get` trap, so they now report it as an own property too. Closes #25963 Co-authored-by: Claude Opus 5.5 --- .../core/src/integrations/functiontostring.ts | 17 ++++--- .../lib/integrations/functiontostring.test.ts | 45 +++++++++++++++++++ .../src/integrations/hono/patchAppRequest.ts | 8 +--- .../src/integrations/hono/patchRoute.ts | 8 +--- .../integrations/hono/sentryOriginalTraps.ts | 26 +++++++++++ .../integrations/hono/wrapMiddlewareSpan.ts | 8 +--- .../hono/sentryOriginalTraps.test.ts | 24 ++++++++++ 7 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 packages/server-utils/src/integrations/hono/sentryOriginalTraps.ts create mode 100644 packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts 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/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/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/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..10ccdb727ac6 100644 --- a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts +++ b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts @@ -11,6 +11,7 @@ import { import { type MiddlewareHandler } from './honoTypes'; import { SENTRY_HONO_MIDDLEWARE } from './createHonoMiddleware'; 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/test/integrations/hono/sentryOriginalTraps.test.ts b/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts new file mode 100644 index 000000000000..183a7820e133 --- /dev/null +++ b/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts @@ -0,0 +1,24 @@ +import { functionToStringIntegration, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { afterAll, describe, expect, it } from 'vitest'; +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()); + }); +}); From b368f76d4c4fa039eb805487c87a79892fb6eb25 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:06:35 +0100 Subject: [PATCH 20/92] fix(server-utils): Count Bedrock prompt-cache tokens as input tokens (#26147) ## What Bedrock input tokens now include tokens read from and written to the prompt cache, for Converse, ConverseStream, and Claude InvokeModel (streamed or not). - Cache read and cache write token counts are now recorded. - Converse, ConverseStream, and Claude InvokeModel spans now record total tokens, like the other AI integrations. ## Why Bedrock and Anthropic report only uncached prompt tokens as input, so cached calls showed far fewer input tokens than were used. Extracted from #26118. --------- Co-authored-by: Claude Opus 5.5 --- .../aws-sdk/services/bedrock-runtime.ts | 75 +++++++++---- .../test/integrations/aws-sdk-bedrock.test.ts | 104 ++++++++++++++++++ 2 files changed, 159 insertions(+), 20 deletions(-) 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 295c0201301e..3ea7ef15d8f0 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 @@ -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 { @@ -254,7 +259,17 @@ 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 { @@ -296,12 +311,18 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, [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]); } @@ -399,7 +420,17 @@ 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; @@ -418,15 +449,21 @@ function setStopReason(span: Span, stopReason: string | undefined): void { } } -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); } } @@ -471,9 +508,7 @@ function recordNovaAttributes(parsedChunk: ParsedChunk, span: Span): void { } 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); - } + setClaudeStreamInputTokens(span, parsedChunk.message?.usage); if (parsedChunk.message?.usage?.output_tokens !== undefined) { span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, parsedChunk.message.usage.output_tokens); } diff --git a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts index 827bd1b60ff0..eceb30d6045d 100644 --- a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts +++ b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts @@ -4,6 +4,11 @@ import { 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'; @@ -156,3 +161,102 @@ describe('BedrockRuntimeServiceExtension stream spans', () => { 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); + }); +}); From 593e5656ee8ba00aa2384f53f787774ec76b2ede Mon Sep 17 00:00:00 2001 From: Ryan McGee <137445630+rjmcgee53192@users.noreply.github.com> Date: Fri, 6 Nov 2026 05:09:38 -0600 Subject: [PATCH 21/92] fix(core): stop normalize() from throwing on symbol and null-prototype errors (#25967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** `normalize()` is documented to always return a value, using the `**non-serializable**` marker when something goes wrong β€” but it could throw itself. Both places that build that marker interpolated the caught value into a template literal, and that conversion throws for a symbol (`Cannot convert a Symbol value to a string`) and for an object with no usable `toString`. A property getter throwing either one is enough to make `normalize()` throw during event preparation. The caught value is now converted with `String()` inside a try/catch, falling back to fixed text, so callers only ever get the marker. **How to review:** One small helper in `packages/core/src/utils/normalize.ts`, used by the two catch blocks there. The new tests in `normalize.test.ts` cover a getter throwing a symbol, a getter throwing a null-prototype object, and the `stringifyValue` catch failing with a symbol. I verified the fix by running the issue's reproduction against the source directly (both cases threw before, both return the marker after, ordinary inputs unchanged). I could not complete a full local `pnpm install` in my environment, so CI will be the first full suite run β€” flagging that honestly rather than claiming a green local run. **Unsure about:** Opening this as a draft without a prior maintainer go-ahead on the issue β€” happy to adjust the approach if you'd rather handle it differently. Closes #25920 Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- packages/core/src/utils/normalize.ts | 18 ++++++++-- .../core/test/lib/utils/normalize.test.ts | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) 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/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', () => { From 6ffdf105cfb26297703dfb06bdb8429597205728 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:13:22 +0100 Subject: [PATCH 22/92] ref(core): Set up log and metric flushing on first capture (#26146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** The `Client` constructor always called `setupWeightBasedFlushing` for logs and metrics, so every bundle carried the log and metric flush, envelope and size-estimation code, even apps that never capture either. Flushing is now set up in `_INTERNAL_captureSerializedLog` / `_INTERNAL_captureSerializedMetric` when a client's buffer is first created. That is the single entry point every log or metric passes through before it can be flushed: `_INTERNAL_captureLog`/`captureMetric`, and direct callers of the serialized variants. Setup still runs before the first `afterCaptureLog`/`afterCaptureMetric` emit. Logs and metrics behave as before: the same 800 KB weight threshold, the idle timer, `_flushInterval`, and draining on `flush()`/`close()`. The only difference is that a client with nothing buffered no longer registers these hooks. Flushing an empty buffer was already a no-op. Savings with esbuild 0.27.7 (minify, ESM, browser, `__SENTRY_DEBUG__: false`) on a tracing-only setup (`BrowserClient` + `browserTracingIntegration`): **2,068 B raw / 445 B br-11** (131,185 β†’ 129,117 B raw). Locally, size-limit gzip for `@sentry/browser` goes from 30,403 to 29,885 B, and for `@sentry/browser (incl. Tracing)` from 52,727 to 52,173 B. **How to review:** The helper and estimators moved unchanged into `utils/weightBasedFlushing.ts`. The "close flushes the logs buffer" test now captures a log first. It used to spy on `_INTERNAL_flushLogsBuffer` with an empty buffer, which no longer reflects the contract. Closes #26145 via @BYK. -- [View Session in Sentry](https://sentry.sentry.io/explore/conversations/agent%3Aced2e9146c6143efef718a62effeb4d1/?project=4510944073809921) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com> --- packages/core/src/client.ts | 159 ------------------ packages/core/src/logs/internal.ts | 3 + packages/core/src/metrics/internal.ts | 9 + .../core/src/utils/weightBasedFlushing.ts | 149 ++++++++++++++++ packages/core/test/lib/client.test.ts | 53 +++++- 5 files changed, 206 insertions(+), 167 deletions(-) create mode 100644 packages/core/src/utils/weightBasedFlushing.ts 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/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/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/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, From b07cc394954f30821728e5feb09354606a9e247e Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:15:08 +0100 Subject: [PATCH 23/92] chore: Add external contributor to CHANGELOG.md (#26171) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #25967 Co-authored-by: andreiborza <168741329+andreiborza@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 822fc26dcded..8e085ac4c1c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg. Thank you for your contribution! +Work in this release was contributed by @lazerg and @rjmcgee53192. Thank you for your contributions! ## 11.11.0 From d2c6f5b237423706207f98a95f80c8e2ad04c655 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 6 Nov 2026 12:20:40 +0100 Subject: [PATCH 24/92] fix(browser): Keep the current origin for `//` paths in `url.full` (#26165) Router integrations set `url.full` with `getAbsoluteUrl`, which resolved a path like `//foo/bar` as a protocol-relative URL, so the first path segment became the host (`https://foo/bar`). `getAbsoluteUrl` now treats a leading `//` as a path on the current origin, the same way #21645 fixed `//` request paths in core. This fixes every router integration that passes a pathname to the helper. The Next.js app router is the one exception: Next.js treats `router.push('//host/x')` as an external URL and navigates to that host, so the app router instrumentation keeps the protocol-relative meaning there. With a basePath, Next.js makes it a local path, and the SDK does the same. Closes #26157 Co-authored-by: Claude Opus 5.5 --- .../src/instrumentation/location.ts | 7 ++++- .../test/instrumentation/location.test.ts | 7 +++++ .../appRouterRoutingInstrumentation.ts | 16 ++++++++-- .../appRouterRoutingInstrumentation.test.ts | 30 +++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) 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/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/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/test/client/appRouterRoutingInstrumentation.test.ts b/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts index 25ecbfdd3f80..4d94c5dd2cdc 100644 --- a/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts +++ b/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts @@ -196,6 +196,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 +280,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'], From b2f42546e6f0175933cf6791a9e8645b237b9f4d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 6 Nov 2026 12:21:03 +0100 Subject: [PATCH 25/92] fix(cloudflare): Capture NonRetryableError before the final workflow step attempt (#26161) Since #21025, `instrumentWorkflowWithSentry` only captures a step error once retries are exhausted. A `NonRetryableError` ends the instance on an earlier attempt, so it was never reported. We now also capture errors the Workflows engine treats as non-retryable, using the engine's own rule (`name === 'NonRetryableError'` or a message starting with `NonRetryableError`) so we don't import `cloudflare:workflows` at runtime. Closes #26091 Co-authored-by: Claude Opus 5.5 --- .../suites/workflows/step-context/index.ts | 9 +- .../suites/workflows/step-context/test.ts | 27 +++++ packages/cloudflare/src/workflows.ts | 20 +++- packages/cloudflare/test/workflow.test.ts | 105 ++++++++++++++++++ 4 files changed, 158 insertions(+), 3 deletions(-) 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/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 { }); }); + 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) From a1a1de0efd587d3727033911cb0cd3018ad65ed0 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:33:31 +0100 Subject: [PATCH 26/92] fix(server-utils): Read Bedrock Claude stream output tokens from message_delta (#26170) ## What Bedrock Claude streams now record the final output token count from `message_delta`. ## Why The count in `message_start` is only a placeholder, so Claude stream spans showed about 1 output token. Extracted from #26118. Co-authored-by: Claude Opus 5.5 --- .../aws-sdk/services/bedrock-runtime.ts | 5 +++-- .../test/integrations/aws-sdk-bedrock.test.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) 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 3ea7ef15d8f0..7259dc085004 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 @@ -509,8 +509,9 @@ function recordNovaAttributes(parsedChunk: ParsedChunk, span: Span): void { function recordClaudeAttributes(parsedChunk: ParsedChunk, span: Span): void { setClaudeStreamInputTokens(span, parsedChunk.message?.usage); - if (parsedChunk.message?.usage?.output_tokens !== undefined) { - span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS, parsedChunk.message.usage.output_tokens); + // `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]); diff --git a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts index eceb30d6045d..a1ba675251e6 100644 --- a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts +++ b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts @@ -151,6 +151,27 @@ describe('BedrockRuntimeServiceExtension stream spans', () => { 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); + }); + it('ends an InvokeModelWithResponseStream span without a modelId when the stream is drained', async () => { const span = startInactiveSpan({ name: 'bedrock' }); const data = { body: (async function* () {})() }; From 25d5da77e95d64ae6759b2464c4f0f44292b3b02 Mon Sep 17 00:00:00 2001 From: adityankale190895 Date: Fri, 6 Nov 2026 17:32:52 +0530 Subject: [PATCH 27/92] test(bundler-plugins): Increase timeout for webpack release-injection test (#26172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** `injects the release only into initial chunks` runs a full `mode: 'production'` webpack build within Vitest's default 5s timeout. On cold Node 20 runners it often goes past that, and it caused 7 of the 10 recent `CI: Build & Test` failures on `develop` I looked at (details in the issue). This gives it the same `{ timeout: 15_000 }` that the sibling `child-compiler.test.ts` already uses. As agreed in the issue, this is a first try; if it still flakes at 15s, the build itself needs a look. Locally the test passes in about 2.6–3.1s with the change (3 runs, Node 24). Closes #26169 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- packages/bundler-plugins/test/webpack/release-injection.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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({ From 5f9835b405b1f08189d0e59ea0749a6b82d3e289 Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:56:43 +0100 Subject: [PATCH 28/92] chore: Add external contributor to CHANGELOG.md (#26179) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #26172 Co-authored-by: andreiborza <168741329+andreiborza@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e085ac4c1c1..0d718d19408c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg and @rjmcgee53192. Thank you for your contributions! +Work in this release was contributed by @lazerg, @rjmcgee53192, and @adityankale190895. Thank you for your contributions! ## 11.11.0 From b0128dc1b4b48ad2204605092a359172cab58940 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 15:12:07 +0100 Subject: [PATCH 29/92] perf(ci): use ARM runners for builds and selected tests (#26106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today our CI uses x86 runners for all our job, this PR moves a selection of our CI runners to the free ARM64 runners to reduce CI time. The jobs that are moved over were determined by doing a few benchmark runs with more details in the tables below. x86 baseline is our CI as it runs today. The ARM run then uses ARM runners as much as possible with the caveat that we cannot move node/bun/deno integration tests to ARM atm because they run SQL Server container tests that do not work on ARM (potentially something we can look into next if we go with this). Running on ARM improves many jobs a lot but is worse for browser and E2E tests, partially because of missing cache hits. That's how I ended up with the "hybrid" configuration that basically takes best of both worlds. | Configuration | Required CI time* | vs x86 | |---|---:|---:| | [x86 baseline](https://github.com/getsentry/sentry-javascript/actions/runs/41988650376) | 10m42s | β€” | | [ARM trial](https://github.com/getsentry/sentry-javascript/actions/runs/41990016190) | 9m47s | 55s faster (8.6%) | | [Hybrid](https://github.com/getsentry/sentry-javascript/actions/runs/41994627106) | **9m31s** | **71s faster (11.1%)** | In the table below you can see in more detail how the job timings changed between the configurations for the latest test run: | Job / group | x86 | ARM trial | |---|---:|---:| | **Required CI completion*** | **10m42s** | **9m47s** | | Build | 1m58s | 1m54s | | Build bundles | 2m52s | 2m21s | | Build tarballs | 3m59s | 2m56s | | Lint | 2m19s | 1m40s | | Size check | 2m22s | 1m29s | | Node unit tests | 4m28s | 3m14s | | Bun unit tests | ~56s | 47s | | Deno unit tests | 1m18s | 1m02s | | Bundler integration | 1m44s | 1m41s | | Browser unit tests | 2m44s | 2m52s | | Browser Playwright | 2m40s | 3m47s | | Browser loader | 1m22s | 1m48s | | E2E | 1m07s | 1m35s | | Node integration† | 4m54s | 4m46s | | Bun integration† | 4m31s | 4m48s | | Deno integration† | 4m15s | 4m39s | \* Excludes initial queueing and optional jobs. Each run had one required-test failure; ARM browser caches were cold. † These integration jobs stayed on x86 in both runs. --------- Co-authored-by: OpenAI Codex --- .../actions/install-dependencies/action.yml | 4 ++- .github/actions/restore-cache/action.yml | 2 +- .github/workflows/build.yml | 31 ++++++++++--------- 3 files changed, 21 insertions(+), 16 deletions(-) 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/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..847d5bf8b1d0 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 @@ -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: @@ -776,6 +776,7 @@ jobs: name: Node (${{ matrix.node }}) Integration Tests (${{ matrix.shard }}/3) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' + # The tedious suite uses a SQL Server container that requires x86-64. runs-on: ubuntu-24.04 timeout-minutes: 20 # Node 27 is an alpha: its ABI and APIs can change, so its failures must not fail the required checks. @@ -868,7 +869,7 @@ 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 @@ -881,7 +882,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,6 +912,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' + # Shared Node suites include the x86-only SQL Server container. runs-on: ubuntu-24.04 timeout-minutes: 15 strategy: @@ -945,6 +947,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' + # Shared Node suites include the x86-only SQL Server container. runs-on: ubuntu-24.04 timeout-minutes: 15 strategy: @@ -990,7 +993,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 +1010,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 From 554124d2b07691622df9ce2961a28f733ebbebd7 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 15:12:08 +0100 Subject: [PATCH 30/92] perf(ci): isolate SQL Server tests and use ARM integration runners (#26148) The previous PR kept node/bun/deno integration tests on x86 because the tedious SQL server tests are not compatible with an ARM runner. This isolates the SQL server tests into a dedicated job running on node 24 and moves the integration tests to ARM runners. Benchmark results below: | Integration group | Parent hybrid (x86) | ARM with SQL Server isolated | Change | |---|---:|---:|---:| | Node, median job | 4m59s | 4m16s | 14% faster | | Bun, median job | 4m35s | 4m10s | 9% faster | | Deno, median job | ~4m46s | ~4m01s | 16% faster | | Slowest Node shard | 7m11s | 5m16s | 27% faster | | Dedicated SQL Server job | Included in existing suites | 1m24s, passed | x86 / Node 24 | This does not translate directly to CI wall time for full runs since these are bottlenecked by E2E/browser tests. However, many runs that do not trigger full CI should still benefit from this. --------- Co-authored-by: OpenAI Codex --- .github/workflows/build.yml | 41 +++++++++++++++---- .../node-suites/excludes.ts | 2 + 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 847d5bf8b1d0..8eba990fdcb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -776,8 +776,7 @@ jobs: name: Node (${{ matrix.node }}) Integration Tests (${{ matrix.shard }}/3) needs: [job_get_metadata, job_build] if: needs.job_build.outputs.changed_node_integration == 'true' || github.event_name != 'pull_request' - # The tedious suite uses a SQL Server container that requires x86-64. - 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' }} @@ -820,7 +819,33 @@ 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 }}/3 --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: @@ -912,8 +937,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' - # Shared Node suites include the x86-only SQL Server container. - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 strategy: fail-fast: false @@ -947,8 +971,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' - # Shared Node suites include the x86-only SQL Server container. - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-arm timeout-minutes: 15 strategy: fail-fast: false @@ -983,7 +1006,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 @@ -1185,6 +1209,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, diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 046e973354dd..86ff4107247b 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -257,6 +257,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, From ee844778de382585901ba1cf3077f396b4c8934f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 15:12:08 +0100 Subject: [PATCH 31/92] perf(ci): minimize Playwright system dependency installation (#26159) **Why:** Many of our CI runs today stall a long time waiting to install ubuntu system dependencies for browsers getting installed during the playwright installation step. Turns out, many of these system requirements are already preinstalled by the GitHub runners we use. In fact, for firefox and chromium ALL for us required system packages are already on the runner and running the system dependency installation step only adds bloat, e.g. font packages that we do not need for our tests. For webkit it's not as easy, but we can still hand-install a subset of the dependencies and significantly reduce the total download size. We get good per job savings: | System-package downloads per fresh job | Before | After | |---|---:|---:| | Chromium | 21.1 MB | 0 | | Firefox | 48.8 MB | 0 | | WebKit | 114 MB / 181 packages | 29 MB / 65 packages | Since this runs on hundreds of browser/E2E test jobs in our CI, we save around 6GB of downloads in one full CI run drastically reducing the likelihood of hitting a slow download mirror. --------- Co-authored-by: OpenAI Codex --- .github/actions/install-playwright/action.yml | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index ca6c8a2d9583..7dfc5482857d 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -38,22 +38,34 @@ 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: Install headless WebKit system dependencies + if: steps.playwright-version.outputs.version != '' && contains(inputs.browsers, 'webkit') + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + 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 shell: bash - working-directory: ${{ runner.temp }} # Only store cache on develop branch - name: Store cached playwright binaries From 18c69eb2f2c401a0d1ebd382a9842a3515374dca Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 15:12:09 +0100 Subject: [PATCH 32/92] perf(ci): (more) sharding of browser and runtime integration tests (#26163) **Why:** Shard or increase sharding of some of the browser and runtime integration tests that became bottlenecks in recent runs. Wall time for full CI runs is expected to only slightly improve, but this should greatly benefit completion time for more targeted runs that only kick off integration tests at the cost of higher overall CI minutes. Chromium CDN variants and WebKit use two shards; required Node versions and Cloudflare use four. Slowest test-execution step in each group, excluding setup and queueing: | Tests | Baseline | More shards | Improvement | |---|---:|---:|---:| | Chromium CDN bundles | 2m30 | 1m53 | 25% | | WebKit | 2m24 | 1m36 | 33% | | Node 20 | 4m48 | 3m56 | 18% | | Node 22 | 4m38 | 3m59 | 14% | | Node 24 | 3m55 | 3m15 | 17% | | Node 26 | 3m39 | 3m08 | 14% | | Cloudflare | 4m09 | 2m04 | 50% | Co-authored-by: OpenAI Codex --- .github/workflows/build.yml | 42 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8eba990fdcb8..20371a0d294e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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,7 +774,7 @@ 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-arm @@ -783,8 +784,19 @@ jobs: 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 @@ -820,7 +832,7 @@ jobs: - name: Run integration tests working-directory: dev-packages/node-integration-tests # Tedious runs in the dedicated SQL Server job because its database container requires x86. - run: pnpm run test --shard=${{ matrix.shard }}/3 --exclude '**/tracing/tedious/**' + run: pnpm run test --shard=${{ matrix.shard }}/${{ matrix.shards }} --exclude '**/tracing/tedious/**' job_sql_server_integration_tests: name: SQL Server Integration Tests (Node 24) @@ -849,7 +861,7 @@ jobs: # 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 @@ -857,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 @@ -884,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. @@ -900,6 +913,7 @@ jobs: fail-fast: false matrix: shard: [1, 2] + shards: [2] wrangler: [latest] steps: *cloudflare_integration_tests_steps From 24aed1b20ba350917cad7f174108ee1853b156aa Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 15:12:09 +0100 Subject: [PATCH 33/92] perf(ci): decouple ordinary E2E tests from the AWS Lambda layer build (#26167) **Why:** Ordinary E2E apps currently wait for the Lambda layer even though only `aws-serverless-layer` needs it. Separate those two variants so the other 267 variants can start once tarballs are ready. This likely doesn't help much with the pipeline as is because usually tarballs finish later than the lambda layer and therefore this doesn't matter. However, once we move to `pnpm pack` tarball packing will be significantly faster and we'll have all E2E test apps waiting for the layer. The impact isn't huge but in my experiments it saves around 20 seconds off the E2E app run with fast packing. --------- Co-authored-by: OpenAI Codex --- .github/actions/prepare-e2e-test/action.yml | 2 +- .github/workflows/build.yml | 31 +++++++++++++++----- dev-packages/e2e-tests/lib/getTestMatrix.mjs | 9 +++++- 3 files changed, 32 insertions(+), 10 deletions(-) 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/workflows/build.yml b/.github/workflows/build.yml index 20371a0d294e..3138bc70bcec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -1089,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 @@ -1161,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 @@ -1170,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. @@ -1232,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/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 From 1652cfe2e71f4f2e570fd50838fbc6c4057f93cd Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:32:02 +0100 Subject: [PATCH 34/92] fix(server-utils): Read Bedrock Mistral and Cohere stream token counts from invocation metrics (#26176) ## What Bedrock Mistral and Cohere streams now record the real input and output token counts that Bedrock sends in the last chunk. If the stream has no counts, the span gets no token attributes. ## Why The output tokens were a text estimate (characters / 6) from the last chunk only, which showed a cost that looked real but was wrong. Extracted from #26118. --------- Co-authored-by: Claude Opus 5.5 --- .../aws-sdk/services/bedrock-runtime.ts | 26 ++++++---- .../test/integrations/aws-sdk-bedrock.test.ts | 48 +++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) 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 7259dc085004..97854b017c8c 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 @@ -542,28 +542,36 @@ function recordLlamaAttributes(parsedChunk: ParsedChunk, span: Span): void { } } -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]); } } 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]); } } 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]); } diff --git a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts index a1ba675251e6..a63e70d3324c 100644 --- a/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts +++ b/packages/server-utils/test/integrations/aws-sdk-bedrock.test.ts @@ -172,6 +172,54 @@ describe('BedrockRuntimeServiceExtension stream spans', () => { 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* () {})() }; From 0708a455947825cdfd6d7a2c8273a3ad8fd12faf Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:03:55 +0100 Subject: [PATCH 35/92] fix(server-utils): Record Bedrock finish reasons as a JSON string (#26184) ## What Bedrock spans now record `gen_ai.response.finish_reasons` as a JSON string, like `'["end_turn"]'`, instead of an array. ## Why The conventions define this attribute as a string, and our other AI integrations already send a JSON string. Extracted from #26118. Co-authored-by: Claude Opus 5.5 --- .../suites/aws-serverless/bedrock/test.ts | 4 +-- .../aws-sdk/services/bedrock-runtime.ts | 33 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) 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..de0d7878c826 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 @@ -34,7 +34,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 +49,7 @@ 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"]'); } describe('awsIntegration - Bedrock', () => { 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 97854b017c8c..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 @@ -296,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) { @@ -308,7 +308,7 @@ 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')) { const usage = responseBody.usage; @@ -324,7 +324,7 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { ), ); 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) { @@ -334,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])); } } } @@ -445,7 +448,7 @@ function setStreamErrorStatus(span: Span, abortSignal: AbortSignal | undefined): 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])); } } @@ -503,7 +506,7 @@ 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])); } } @@ -514,7 +517,7 @@ function recordClaudeAttributes(parsedChunk: ParsedChunk, span: Span): void { 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])); } } @@ -526,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])); } } @@ -538,7 +541,7 @@ 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])); } } @@ -559,20 +562,20 @@ function recordInvocationMetrics(parsedChunk: ParsedChunk, span: Span): void { 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 { 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 { 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])); } } From 5f6c68426c15bfb44b05a6aaada41af2871bf194 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:04:10 +0100 Subject: [PATCH 36/92] fix(server-utils): Record Vercel AI system instructions from message objects (#26181) ## What Vercel AI spans now record `gen_ai.system_instructions` when the system prompt is a message object or an array of messages. ## Why The AI SDK accepts these forms, and they are the documented way to set provider options such as Anthropic prompt caching. The spans lost the system prompt for these calls. Closes: #26079 --------- Co-authored-by: Claude Opus 5.5 --- .../v6_v7/scenario-system-message.mjs | 42 +++++++++++++++++++ .../suites/tracing/vercelai/v6_v7/test.ts | 30 +++++++++++++ .../vercel-ai/vercel-ai-dc-subscriber.ts | 15 +++++-- .../vercel-ai-orchestrion-subscriber.ts | 2 +- 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-system-message.mjs 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/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..64e9175226de 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 @@ -945,15 +945,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), From 6eab8d56ff03bf264575920d939ccc93e1643794 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 6 Nov 2026 16:07:07 +0100 Subject: [PATCH 37/92] fix(ci): refresh lockfile deprecation metadata (#26183) **Why:** Develop's `pnpm dedupe --check` fails because the lockfile lacks the registry deprecation notice for the already-locked `image-size@0.5.5`. Refresh that metadata without changing package versions or dependency resolutions. Co-authored-by: OpenAI Codex --- pnpm-lock.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de50736bca0c..56fb4ff974aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13056,6 +13056,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: From 64f5b60b9b87a76ceedd1f9797a93b66d178d8e5 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:31:16 +0100 Subject: [PATCH 38/92] chore(deps): Bump `@sentry/conventions` to 0.31.0 (#26160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** 0.31.0 adds a new `@sentry/conventions/metrics` entry point and a few attributes we were already emitting as hardcoded strings or local stand-ins. This bump switches those call sites to the published constants so the names stay tied to the spec. Emitted names and values don't change. Nothing was removed or renamed upstream. The two new deprecations (`GEN_AI_HANDOFF`, `SENTRY_USER_GEO_SUBREGION`) aren't used in the SDK. **How to review:** The version bumps keep each file's caret or exact pin, and `minimumReleaseAgeExclude` moves to 0.31.0. The only source change that isn't a plain constant swap is in the Vercel AI subscriber: `contextAttributes` now gets the `*_KEY_BASE` constant and adds the `.` separator itself. **Unsure about:** The `nextjs-16-cacheComponents` and `nextjs-16-streaming-cacheComponents` e2e apps still pin `0.27.0`, as they did before this change. I left them alone. BullMQ's own metric names (`messaging.bullmq.jobs.*`) still come from `toMessagingKey`, so only the literal `messaging.process.duration` moved to a constant there. via [automation `sched_ae2548aa50f44ee9e080350e80e0d775`](https://junior-prod.sentry.dev/automations/sched_ae2548aa50f44ee9e080350e80e0d775) Β· Run ID `sched_ae2548aa50f44ee9e080350e80e0d775:1793952000000`. -- [View Session](https://junior-prod.sentry.dev/conversations/agent-dispatch%3Adispatch_11d67ef330a2f94f901d7a022c5c1980) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent-dispatch%3Adispatch_11d67ef330a2f94f901d7a022c5c1980/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../browser-integration-tests/package.json | 2 +- .../cloudflare-integration-tests/package.json | 2 +- .../deno-integration-tests/package.json | 2 +- .../test-applications/angular-19/package.json | 2 +- .../cloudflare-pi-durable/package.json | 2 +- .../package.json | 2 +- .../gen-ai-libraries/package.json | 2 +- .../nextjs-16-adapter/package.json | 2 +- .../nextjs-16-tunnel/package.json | 2 +- .../test-applications/nitro-3/package.json | 2 +- .../test-applications/node-eve/package.json | 2 +- .../test-applications/node-hapi/package.json | 2 +- .../node-mastra/package.json | 2 +- .../node-openai-decisions/package.json | 2 +- .../node-pi-durable/package.json | 2 +- .../nuxt-4-static/package.json | 2 +- .../solidstart-static/package.json | 2 +- .../node-integration-tests/package.json | 2 +- packages/angular/package.json | 2 +- packages/astro/package.json | 2 +- packages/aws-serverless/package.json | 2 +- packages/browser-utils/package.json | 2 +- .../src/performance/elementTiming.ts | 5 +- .../test/performance/elementTiming.test.ts | 7 +- packages/browser/package.json | 2 +- .../src/integrations/bfcacheMetrics.ts | 11 +- packages/bun/package.json | 2 +- packages/cloudflare/package.json | 2 +- packages/core/package.json | 2 +- packages/deno/package.json | 2 +- packages/effect/package.json | 2 +- packages/elysia/package.json | 2 +- packages/ember/package.json | 2 +- packages/google-cloud-serverless/package.json | 2 +- packages/hono/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- .../src/client/routing/traceMetaTags.ts | 5 +- .../appRouterRoutingInstrumentation.test.ts | 14 +- .../nextjs/test/client/traceMetaTags.test.ts | 8 +- packages/nitro/package.json | 2 +- packages/node/package.json | 2 +- .../src/integrations/tracing/bullmq/meter.ts | 3 +- .../test/integrations/tracing/bullmq.test.ts | 20 ++- packages/nuxt/package.json | 2 +- packages/opentelemetry/package.json | 2 +- packages/profiling-node/package.json | 2 +- packages/react-router/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/replay-internal/package.json | 2 +- packages/server-utils/package.json | 2 +- .../vercel-ai/vercel-ai-dc-subscriber.ts | 12 +- packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/vue/package.json | 2 +- pnpm-lock.yaml | 150 +++++++++--------- pnpm-workspace.yaml | 2 +- 61 files changed, 177 insertions(+), 160 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 76d4fcc337ce..f00f5d47d843 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -61,7 +61,7 @@ "@playwright/test": "1.64.1", "@sentry/browser": "11.11.0", "@sentry/browser-utils": "11.11.0", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.31.0", "@sentry/core": "11.11.0", "@sentry/replay": "11.11.0", "@sentry/replay-canvas": "11.11.0", diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 7e1160804461..b85215f9fd08 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -28,7 +28,7 @@ "@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.31.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", "ai": "^6.0.0", diff --git a/dev-packages/deno-integration-tests/package.json b/dev-packages/deno-integration-tests/package.json index ad5f92931816..b2597575f3c1 100644 --- a/dev-packages/deno-integration-tests/package.json +++ b/dev-packages/deno-integration-tests/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@sentry-internal/node-integration-tests": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.31.0", "@sentry/core": "11.11.0", "@sentry/deno": "11.11.0", "mysql": "^2.18.1", 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..fb5c68db2e3c 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -36,7 +36,7 @@ "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", "@playwright/test": "1.64.1", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.31.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", 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..455b99384a8f 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 @@ -22,7 +22,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.31.0", "typescript": "^5.5.2", "vite": "7.3.5", "wrangler": "^4.136.2" 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..9c4d7b9ea3c4 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.31.0", "@types/node": "^26.1.2", "sentry": "~0.44.1", "typescript": "~6.0.3", 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..cfa72e11537f 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.31.0", "@types/express": "^4.17.21", "@types/node": "^18.19.1", "tsx": "4.21.0", 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..03ef2f7c040e 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.31.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", 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..c079ce13dadd 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 @@ -41,7 +41,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.31.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", 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..7b9cdcc38a89 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.31.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..4779ef476d7c 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/package.json +++ b/dev-packages/e2e-tests/test-applications/node-eve/package.json @@ -32,7 +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/conventions": "0.31.0", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "24.x", "typescript": "~5.9.0" 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..ee464da88bd0 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.31.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..f31a2ab9a7d0 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.31.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..62089c547f8e 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.31.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..12fa14dcc544 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,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.31.0", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "24.x" }, 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..2e0d9daf20ba 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.31.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz" }, "sentryTest": { 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..3c222da22783 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry/conventions": "0.30.0", + "@sentry/conventions": "0.31.0", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index e3e8bc7beeb0..e801ce94c331 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -61,7 +61,7 @@ "@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.31.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", "@sentry/node": "11.11.0", diff --git a/packages/angular/package.json b/packages/angular/package.json index d8f5de765310..360f2baaffa1 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.31.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index b090296ee81a..eda3fbd46261 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.31.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index d7bb03651d61..66fd00940a84 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 100f346bfe00..7c88091ad10f 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.31.0", "web-vitals": "^6.2.3" }, "devDependencies": { 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/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..75315a5477dd 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.31.0", "@sentry/feedback": "11.11.0", "@sentry/replay": "11.11.0", "@sentry/replay-canvas": "11.11.0", 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..7cbd909d7b53 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.31.0", "@sentry/node": "11.11.0", "@sentry/server-utils": "11.11.0" }, diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 3abd81222111..d6a219cd9bdd 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.31.0", "@sentry/core": "11.11.0", "@sentry/opentelemetry": "11.11.0", "@sentry/server-utils": "11.11.0", diff --git a/packages/core/package.json b/packages/core/package.json index faaace218f88..e434a7151013 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -92,6 +92,6 @@ "zod": "^4.6.7" }, "dependencies": { - "@sentry/conventions": "^0.30.0" + "@sentry/conventions": "^0.31.0" } } diff --git a/packages/deno/package.json b/packages/deno/package.json index 7e18a3598c30..d96b09353a3b 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.31.0", "@sentry/core": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0" diff --git a/packages/effect/package.json b/packages/effect/package.json index 3fc4f30e7425..35ecd497d664 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0" }, diff --git a/packages/elysia/package.json b/packages/elysia/package.json index d4ba6ac3a7f9..084561c8869c 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.31.0" }, "peerDependencies": { "elysia": "^1.4.0" diff --git a/packages/ember/package.json b/packages/ember/package.json index e8f1d201a9af..b4c15359e379 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -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.31.0", "@sentry/core": "11.11.0", "decorator-transforms": "^2.3.2" }, diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index d85aebf1039e..1970592e6c6c 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/hono/package.json b/packages/hono/package.json index 9566f0d14e25..f59c42626665 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.31.0", "@sentry/server-utils": "11.11.0" }, "peerDependencies": { diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 4f9309508958..b82a41e601d6 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-utils": "11.11.0" diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index d94711c5eac6..fed113b51450 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/opentelemetry": "11.11.0", 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/test/client/appRouterRoutingInstrumentation.test.ts b/packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts index 4d94c5dd2cdc..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'; @@ -405,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]); }); @@ -423,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]); }); @@ -436,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/nitro/package.json b/packages/nitro/package.json index 69c6d2a68c65..e1a869e5ee75 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/node/package.json b/packages/node/package.json index 8e03e232e2de..acf5a4cb0565 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.31.0", "@sentry/core": "11.11.0", "@sentry/opentelemetry": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", 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/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..cd9e1508ce4b 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/bundler-plugins": "11.11.0", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 48fb05517a03..4b9f10734ceb 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.31.0", "@sentry/core": "11.11.0" }, "peerDependencies": { diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 30caa5cf2ac3..0c0dd47a7b73 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -50,7 +50,7 @@ "test:watch": "vitest --watch" }, "dependencies": { - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.31.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..6a2ffbf51eb0 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -57,7 +57,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.30.0", + "@sentry/conventions": "^0.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", diff --git a/packages/react/package.json b/packages/react/package.json index 9cb321d1ea4f..488021aba4e7 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.31.0" }, "peerDependencies": { "react": "17.x || 18.x || 19.x", diff --git a/packages/remix/package.json b/packages/remix/package.json index c45f348f7171..428b85e0a300 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,7 +78,7 @@ "@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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index d37425f6c4a9..051253e150bb 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -74,7 +74,7 @@ "@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/conventions": "^0.31.0", "@sentry/rrweb": "2.45.0", "@sentry/rrweb-snapshot": "2.45.0", "fflate": "0.8.3", diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 2490450b3313..8386c81a0ee2 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.31.0", "@sentry/core": "11.11.0" }, "devDependencies": { 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 64e9175226de..5c12ef8f98c4 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, @@ -86,8 +88,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 +459,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 +541,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 +549,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 +629,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, diff --git a/packages/solid/package.json b/packages/solid/package.json index 97945931f33d..c18088cb9dce 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.31.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index b698a4585010..021946afc6eb 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.31.0", "h3": "^1.15.10" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c17280b6770a..2c1e323bc190 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.31.0", "@sentry/core": "11.11.0", "magic-string": "~0.30.0" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 5e1bc856e5fe..1760c9d60ab0 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.31.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index f056cfdf6c50..dfe319e84304 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.31.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", diff --git a/packages/vue/package.json b/packages/vue/package.json index d18f6c9e213f..5bcea0e741dd 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.31.0" }, "peerDependencies": { "@tanstack/vue-router": "^1.64.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56fb4ff974aa..1e247feb36f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,8 +301,8 @@ importers: specifier: 11.11.0 version: link:../../packages/browser-utils '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -520,8 +520,8 @@ importers: specifier: 11.11.0 version: link:../../packages/cloudflare '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -559,8 +559,8 @@ importers: specifier: 11.11.0 version: link:../node-integration-tests '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -700,8 +700,8 @@ importers: specifier: 11.11.0 version: link:../../packages/aws-serverless '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -958,8 +958,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1019,8 +1019,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1047,8 +1047,8 @@ importers: packages/aws-serverless: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1081,8 +1081,8 @@ importers: specifier: 11.11.0 version: link:../browser-utils '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1109,8 +1109,8 @@ importers: packages/browser-utils: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1128,8 +1128,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1211,8 +1211,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1248,8 +1248,8 @@ importers: packages/core: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 devDependencies: '@sentry-internal/rollup-utils': specifier: 11.11.0 @@ -1264,8 +1264,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1286,8 +1286,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1311,8 +1311,8 @@ importers: specifier: 11.11.0 version: link:../bun '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1339,8 +1339,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1488,8 +1488,8 @@ importers: packages/google-cloud-serverless: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1528,8 +1528,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1581,8 +1581,8 @@ importers: packages/nestjs: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1627,8 +1627,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1679,8 +1679,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1719,8 +1719,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1775,8 +1775,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1836,8 +1836,8 @@ importers: packages/opentelemetry: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1852,8 +1852,8 @@ importers: packages/profiling-node: dependencies: '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1877,8 +1877,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1953,8 +1953,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2014,8 +2014,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2110,8 +2110,8 @@ importers: specifier: 11.11.0 version: link:../../dev-packages/rollup-utils '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/rrweb': specifier: 2.45.0 version: 2.45.0 @@ -2178,8 +2178,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2212,8 +2212,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2255,8 +2255,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: 0.30.0 - version: 0.30.0 + specifier: 0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2328,8 +2328,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2365,8 +2365,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2429,8 +2429,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2497,8 +2497,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.30.0 - version: 0.30.0 + specifier: ^0.31.0 + version: 0.31.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -8136,8 +8136,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.31.0': + resolution: {integrity: sha512-TOXB7z08JPVop4eiYLhZhgWitUYUMX6T1SjLCthaa8YLnNyA3+gbT1ccUinf2EWvzFwBuYPShPB7oYSkKeCFIQ==} engines: {node: '>=14'} '@sentry/node-cpu-profiler@2.4.4': @@ -26931,7 +26931,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@sentry/conventions@0.30.0': {} + '@sentry/conventions@0.31.0': {} '@sentry/node-cpu-profiler@2.4.4': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 42cc03476a2d..741d003eefef 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -121,7 +121,7 @@ allowBuilds: workerd: true minimumReleaseAgeExclude: - '@opentelemetry/api@1.10.0' - - '@sentry/conventions@0.30.0' + - '@sentry/conventions@0.31.0' - '@sentry/rrdom@2.45.0' - '@sentry/rrweb-snapshot@2.45.0' - '@sentry/rrweb-types@2.45.0' From a9ae8f420335c3678331529fb76669d21b7e5ddb Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:50:43 +0100 Subject: [PATCH 39/92] fix(server-utils): Record LangGraph finish reasons as a JSON string (#26185) ## What LangGraph spans now record `gen_ai.response.finish_reasons` as a JSON string, like `'["stop"]'`, instead of an array. ## Why The conventions define this attribute as a string, and our other AI integrations already send a JSON string. Co-authored-by: Claude Opus 5.5 --- .../node-integration-tests/suites/tracing/langgraph/test.ts | 2 ++ packages/server-utils/src/ai/langgraph/utils.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) 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 eeb6772cc01c..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,6 +6,7 @@ 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, @@ -114,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', ); 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])); } } } From 3963e60a525f38d40381c167bd6c3a802e58f5d7 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:51:06 +0100 Subject: [PATCH 40/92] fix(server-utils): Record Anthropic system instructions from text blocks (#26182) fix(server-utils): Record Anthropic system instructions from text blocks --- .../server-utils/src/ai/anthropic-ai/utils.ts | 16 ++++++++++++++-- .../test/ai/lib/utils/anthropic-utils.test.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) 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/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({ From 2f26678788b7f248144bff1bdbd4c2201301581b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 6 Nov 2026 20:39:50 +0100 Subject: [PATCH 41/92] chore(deps): Bump nanoid to fix CVE-2026-73086 (#26173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Fixes Dependabot alert [3275](https://github.com/getsentry/sentry-javascript/security/dependabot/3275) (nanoid integer overflow, high). Bumps nanoid 5.1.9 β†’ 5.1.16, which is within the `^5.1.7` range of `@size-limit/esbuild` and `@size-limit/webpack`, so it only touches the lockfile. Dev-only (size-limit tooling). Co-authored-by: Claude Opus 5.5 --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e247feb36f4..633ea5452112 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14763,8 +14763,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 @@ -27011,7 +27011,7 @@ 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))': @@ -27020,7 +27020,7 @@ snapshots: '@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)': 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) transitivePeerDependencies: @@ -35285,7 +35285,7 @@ snapshots: nanoid@3.3.20: {} - nanoid@5.1.9: {} + nanoid@5.1.16: {} nanospinner@1.2.2: dependencies: From c536e1e406ed852b6a501e0496f2567bae1a0ed4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 6 Nov 2026 20:33:02 +0000 Subject: [PATCH 42/92] feat(bundler-plugins): Enable hidden source maps and remove them after upload in the Rollup plugin (#25645) Part of the stack for getsentry/sentry-javascript#25609. When the Rollup or Rolldown output has no `sourcemap` setting and an upload can run, the plugin switches the output to hidden source maps, copies each chunk and its map into the upload staging folder from a `generateBundle` hook that runs last, and drops the map asset so it is never written. A map the plugin turned on never reaches the output, not even for chunks that `sourcemaps.assets` or `sourcemaps.ignore` keep out of the upload or that the plugin cannot prepare. `deleteSourcemapsAfterUpload: true` also removes maps the user enabled, but only for chunks that were uploaded; the rest stay, with a warning for a chunk the plugin cannot prepare. `generate()` drops the maps the plugin turned on, so callers that write the result themselves get what they configured. Decisions and stagings are recorded per output, told apart by `format`, `entryFileNames`, `chunkFileNames` and `file`, because several outputs can write into one directory at once with different settings. Staging applies `sourcemaps.assets` and `sourcemaps.ignore` through the matcher that `glob` exports for its own `ignore` option, with the same dot-file rules as the upload from disk: `assets` skips dot files and folders unless a pattern names them, and `ignore` matches them. SvelteKit and Nuxt pass `deleteSourcemapsAfterUpload: false` because they need the map files on disk after the Vite build; the other framework SDKs set the bundler's source map option themselves, which the plugin never deletes, so their builds do not change until each one migrates in its own PR. Integration fixtures cover `ignore` and `assets` on the staged path and mixed outputs into one directory; the CLI stub change comes from #25916 and drops out when that merges. --- CHANGELOG.md | 17 + ...tiple-outputs-mixed-sourcemaps.config.d.ts | 2 + ...ultiple-outputs-mixed-sourcemaps.config.js | 7 + .../sourcemaps-assets-auto.config.d.ts | 2 + .../configs/sourcemaps-assets-auto.config.js | 10 + .../sourcemaps-ignore-auto.config.d.ts | 2 + .../configs/sourcemaps-ignore-auto.config.js | 10 + .../fixtures/rolldown/basic-cjs.test.ts | 1 + .../fixtures/rolldown/basic.test.ts | 1 + ...ultiple-outputs-mixed-sourcemaps.config.ts | 23 + .../multiple-outputs-mixed-sourcemaps.test.ts | 35 ++ .../rolldown/release-disabled.test.ts | 1 + .../rolldown/sourcemaps-assets-auto.config.ts | 12 + .../rolldown/sourcemaps-assets-auto.test.ts | 31 ++ .../rolldown/sourcemaps-ignore-auto.config.ts | 12 + .../rolldown/sourcemaps-ignore-auto.test.ts | 34 ++ .../fixtures/rollup3/basic-cjs.test.ts | 1 + .../fixtures/rollup3/basic.test.ts | 1 + .../fixtures/rollup3/release-disabled.test.ts | 1 + .../fixtures/rollup4/basic-cjs.test.ts | 1 + .../fixtures/rollup4/basic.test.ts | 1 + ...ultiple-outputs-mixed-sourcemaps.config.js | 23 + .../multiple-outputs-mixed-sourcemaps.test.ts | 35 ++ .../fixtures/rollup4/release-disabled.test.ts | 1 + .../rollup4/sourcemaps-assets-auto.config.js | 12 + .../rollup4/sourcemaps-assets-auto.test.ts | 31 ++ .../rollup4/sourcemaps-ignore-auto.config.js | 12 + .../rollup4/sourcemaps-ignore-auto.test.ts | 34 ++ .../fixtures/sentry-stub/index.cjs | 20 + .../src/core/build-plugin-manager.ts | 53 +- packages/bundler-plugins/src/core/index.ts | 1 + packages/bundler-plugins/src/core/types.ts | 4 + .../bundler-plugins/src/core/upload-filter.ts | 65 +++ packages/bundler-plugins/src/rollup/index.ts | 69 ++- .../src/rollup/source-map-handling.ts | 288 ++++++++++ .../test/core/upload-filter.test.ts | 127 +++++ .../test/core/upload-staging.test.ts | 129 ++++- .../delete-sourcemaps-after-upload.test.ts | 513 ++++++++++++++++++ .../test/rollup/public-api.test.ts | 10 +- .../test/rollup/source-map-handling.test.ts | 99 ++++ .../buildTimeOptionsBase.ts | 4 + packages/nuxt/src/vite/sourceMapDeletion.ts | 1 + .../test/vite/sourceMaps-nuxtHooks.test.ts | 1 + packages/sveltekit/src/vite/sourceMaps.ts | 1 + .../sveltekit/test/vite/sourceMaps.test.ts | 2 + 45 files changed, 1696 insertions(+), 44 deletions(-) create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.d.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/multiple-outputs-mixed-sourcemaps.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.d.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets-auto.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.d.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore-auto.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/multiple-outputs-mixed-sourcemaps.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets-auto.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore-auto.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/multiple-outputs-mixed-sourcemaps.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets-auto.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore-auto.test.ts create mode 100644 packages/bundler-plugins/src/core/upload-filter.ts create mode 100644 packages/bundler-plugins/src/rollup/source-map-handling.ts create mode 100644 packages/bundler-plugins/test/core/upload-filter.test.ts create mode 100644 packages/bundler-plugins/test/rollup/delete-sourcemaps-after-upload.test.ts create mode 100644 packages/bundler-plugins/test/rollup/source-map-handling.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d718d19408c..b899ad5b4f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ Work in this release was contributed by @lazerg, @rjmcgee53192, and @adityankale190895. Thank you for your contributions! +### 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. + ## 11.11.0 ### Important Changes 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-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/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-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/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-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/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/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..28de6c6ee95e 100644 --- a/packages/bundler-plugins/src/core/types.ts +++ b/packages/bundler-plugins/src/core/types.ts @@ -200,6 +200,10 @@ 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 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/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index 227f162eae8f..7306d9b7dfab 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -21,6 +21,7 @@ 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'; // The subset of Rollup's `TransformResult` that this plugin's `transform` // hook actually returns. Defined locally instead of imported from `rollup` @@ -81,10 +82,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 +110,10 @@ export function _rollupPluginInternal( if (options.disable) { return { - name: 'sentry-noop-plugin', + main: { + name: 'sentry-noop-plugin', + }, + post: undefined, }; } @@ -116,6 +124,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 +264,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 +283,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 +315,44 @@ export function _rollupPluginInternal( } : transform; - return { + const main = { name, buildStart, ...(shouldTransform ? { transform: transformHook } : {}), + // Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin + // decides in its `config` hook instead. + ...(buildTool === 'rollup' ? { outputOptions: sourceMapHandling.outputOptions } : {}), renderChunk, generateBundle: { order: 'pre' as const, handler: generateBundle }, writeBundle, }; + + const post = { + name: `sentry-${buildTool}-upload-staging`, + generateBundle: { order: 'post' as const, handler: sourceMapHandling.stageSourceMaps }, + }; + + 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/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/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/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index 49eaae2b7916..13e9346243fb 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -256,6 +256,10 @@ 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. */ deleteSourcemapsAfterUpload?: boolean; 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/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/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 7cfa82ed6f6d..7afd0ff088d1 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, }, }; diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index d4f2d3c06db8..5c6134f71ca5 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -385,6 +385,7 @@ describe('deleteFilesAfterUpload', () => { }, sourcemaps: { filesToDeleteAfterUpload: expect.any(Promise), + deleteSourcemapsAfterUpload: false, }, }); @@ -463,6 +464,7 @@ describe('deleteFilesAfterUpload', () => { }, sourcemaps: { filesToDeleteAfterUpload: expect.any(Promise), + deleteSourcemapsAfterUpload: false, }, }); From 03492d26b5d946b4fa2785151c93c6fb30a0a519 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 6 Nov 2026 21:17:33 +0000 Subject: [PATCH 43/92] feat(bundler-plugins): Enable hidden source maps and remove them after upload in the Vite plugin (#25646) Part of the stack for getsentry/sentry-javascript#25609. Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin decides on `build.sourcemap` in a `config` hook on the post plugin, which runs after framework plugins that set `build.sourcemap` themselves. `configResolved` then records one decision per output of `rollupOptions.output` and per Vite 6 environment, and the shared Rollup `generateBundle` hook does the staging and logs the decision, so builds with `build.write: false` and environments that are not built stay quiet. An output or environment that sets `sourcemap` itself is never changed; only hidden maps the user asked to delete are removed there. A client build and an SSR build of the same plugin instances stage on their own, keyed by output directory. Builds with `build.write: false` do not create a staging folder. Integration fixtures cover `sourcemaps.ignore` and `sourcemaps.assets` on the staged path for Vite 8. --- .../fixtures/vite4/basic-cjs.test.ts | 1 + .../fixtures/vite4/basic.test.ts | 1 + .../fixtures/vite4/release-disabled.test.ts | 1 + .../fixtures/vite7/basic-cjs.test.ts | 1 + .../fixtures/vite7/basic.test.ts | 1 + .../fixtures/vite7/release-disabled.test.ts | 1 + .../fixtures/vite8/basic-cjs.test.ts | 1 + .../fixtures/vite8/basic.test.ts | 1 + .../fixtures/vite8/release-disabled.test.ts | 1 + .../vite8/sourcemaps-assets-auto.config.ts | 18 + .../vite8/sourcemaps-assets-auto.test.ts | 31 ++ .../vite8/sourcemaps-ignore-auto.config.ts | 18 + .../vite8/sourcemaps-ignore-auto.test.ts | 34 ++ packages/bundler-plugins/package.json | 1 + packages/bundler-plugins/src/rollup/index.ts | 31 +- packages/bundler-plugins/src/vite/index.ts | 35 +- .../src/vite/source-map-hooks.ts | 147 ++++++++ .../delete-sourcemaps-after-upload.test.ts | 348 ++++++++++++++++++ .../test/vite/public-api.test.ts | 2 +- .../solidstart/test/config/withSentry.test.ts | 3 + .../test/vite/sentrySolidStart.test.ts | 1 + .../test/vite/sentrySolidStartVite.test.ts | 1 + .../test/vite/sentrySvelteKitPlugins.test.ts | 7 +- pnpm-lock.yaml | 3 + 24 files changed, 653 insertions(+), 36 deletions(-) create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets-auto.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore-auto.test.ts create mode 100644 packages/bundler-plugins/src/vite/source-map-hooks.ts create mode 100644 packages/bundler-plugins/test/vite/delete-sourcemaps-after-upload.test.ts 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-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/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index c683200dcbeb..4a1481196d1f 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -145,6 +145,7 @@ "@types/node": "^20.19.0", "premove": "^4.0.0", "rolldown": "^1.0.0", + "vite": "^8.3.1", "vitest": "^4.1.11", "webpack": "5.112.2" }, diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index 7306d9b7dfab..7017577c6919 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -22,6 +22,7 @@ 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` @@ -315,21 +316,41 @@ export function _createRollupPluginInternals( } : transform; + // 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 } : {}), - // Vite warns about `rollupOptions.output.sourcemap`, so the Vite plugin - // decides in its `config` hook instead. - ...(buildTool === 'rollup' ? { outputOptions: sourceMapHandling.outputOptions } : {}), + ...(viteSourceMapHooks + ? { configResolved: viteSourceMapHooks.configResolved } + : { outputOptions: sourceMapHandling.outputOptions }), renderChunk, generateBundle: { order: 'pre' as const, handler: generateBundle }, writeBundle, }; const post = { - name: `sentry-${buildTool}-upload-staging`, - generateBundle: { order: 'post' as const, handler: sourceMapHandling.stageSourceMaps }, + 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 }; 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/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/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/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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 633ea5452112..4e5e575b931a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1198,6 +1198,9 @@ importers: rolldown: specifier: ^1.0.0 version: 1.2.16 + vite: + specifier: ^8.3.1 + version: 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) 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) From f72eb6c6fa41eae4b481e1fa4d8e94dabf9cb2bc Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 6 Nov 2026 17:46:05 -0500 Subject: [PATCH 44/92] feat(core): Add the primitives to answer managed tunnel requests in the runtimes (#25997) These series of PRs re-work the new tunnel implementation a second time, it acts as improvement because none of the changes are breaking. ### What's different? The new approach here, sets us up to intercept the tunnel request on the runtime level. This means it is much earlier than the current middleware approach, the tunnel request shouldn't hit the user application at all. This allows the `serveTunnelIntegration` to be able to set up the tunnel handling on its own without requiring the user to add a manual handler on their own. For frameworks it doesn't change much, but for arbitrary server setups, it becomes much easier: ```ts // that's it! Sentry.init({ integrations: [ Sentry.serveTunnelIntegration() ] }); ``` There is no need to setup routes or handlers unless the user explicity turns it off by either not using the integration or by turning the interception off. ```ts // integration still adds some needed ignoreSpan rules // to avoid tunnel requests from showing up in their traces Sentry.serveTunnelIntegration({ intercept: false }) ``` ### This PR This PR adds primitive utilities and adjusts the API in a way to make it setup the changes up the stack. --- .../managed-tunnel-app-errors/index.ts | 30 ++++ .../tracing/managed-tunnel-app-errors/test.ts | 23 +++ .../managed-tunnel-app-errors/wrangler.jsonc | 6 + .../managed-tunnel/instrument-forward.mjs | 10 ++ .../instrument-intercept-false.mjs | 10 ++ .../tracing/managed-tunnel/server-forward.mjs | 13 ++ .../managed-tunnel/server-intercept-false.mjs | 19 +++ .../suites/tracing/managed-tunnel/test.ts | 160 +++++++++++++++++- .../astro/src/integration/middleware/index.ts | 2 +- packages/astro/src/integration/tunnel.ts | 2 +- packages/astro/src/tunnel.ts | 4 +- packages/core/src/carrier.ts | 4 + packages/core/src/index.ts | 2 +- .../integrations/http/server-subscription.ts | 10 ++ packages/core/src/integrations/serveTunnel.ts | 6 +- packages/core/src/server-runtime-client.ts | 4 +- packages/core/src/server.ts | 8 +- packages/core/src/types/options.ts | 9 + packages/core/src/utils/tunnel.ts | 113 ++++++++----- ...gedTunnelDefaults.ts => tunnelDefaults.ts} | 56 +++--- packages/core/src/utils/tunnelForward.ts | 63 ++++++- packages/core/src/utils/tunnelServer.ts | 36 ++++ packages/core/src/utils/tunnelStore.ts | 62 +++++++ packages/core/test/lib/utils/tunnel.test.ts | 138 +++++++++++---- packages/nitro/src/module.ts | 4 +- packages/nitro/src/runtime/plugins/server.ts | 4 +- packages/nitro/test/module.test.ts | 4 +- packages/nuxt/src/common/tunnel.ts | 4 +- .../runtime/plugins/tunnel-legacy.server.ts | 4 +- .../nuxt/src/runtime/plugins/tunnel.server.ts | 4 +- packages/react-router/src/client/sdk.ts | 4 +- .../react-router/src/server/tunnelRoute.ts | 4 +- .../test/server/tunnelRoute.test.ts | 4 +- .../src/utils/wrapServerRequestHandler.ts | 35 +++- packages/solidstart/src/common/tunnel.ts | 4 +- packages/solidstart/src/nitro/tunnelPlugin.ts | 4 +- .../test/nitro/tunnelPlugin.test.ts | 4 +- packages/sveltekit/src/common/tunnel.ts | 4 +- .../sveltekit/src/server-common/handle.ts | 4 +- .../test/server-common/handle.test.ts | 4 +- .../src/client/tunnelRoute.ts | 4 +- .../src/server/tunnelRoute.ts | 4 +- .../src/vite/tunnelRoute.ts | 4 +- .../wrapServerEntryWithSentryTunnel.test.ts | 4 +- .../test/vite/tunnelRoute.test.ts | 4 +- 45 files changed, 723 insertions(+), 182 deletions(-) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tracing/managed-tunnel-app-errors/wrangler.jsonc create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-forward.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-intercept-false.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-forward.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-intercept-false.mjs rename packages/core/src/utils/{managedTunnelDefaults.ts => tunnelDefaults.ts} (59%) create mode 100644 packages/core/src/utils/tunnelServer.ts create mode 100644 packages/core/src/utils/tunnelStore.ts 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/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/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/test.ts b/dev-packages/node-integration-tests/suites/tracing/managed-tunnel/test.ts index 1504ba345677..13510989dd0d 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,8 +1,93 @@ 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(); @@ -26,4 +111,77 @@ describe('managed 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-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/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/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/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/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/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/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index af5826fb6c86..0cc1f279cdf0 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -24,6 +24,7 @@ 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 { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from '../../currentScopes'; import { hasSpansEnabled } from '../../utils/hasSpansEnabled'; import { @@ -132,6 +133,15 @@ export function instrumentServer(options: HttpInstrumentationOptions, server: Ht return target.apply(thisArg, args); } + // Tunnel requests record no session or span. The app may still answer them (`intercept: false`), so they keep + // their own isolation scope. + if (isServedTunnelRequest(request)) { + 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); diff --git a/packages/core/src/integrations/serveTunnel.ts b/packages/core/src/integrations/serveTunnel.ts index 3994b35a252d..afe614cbcbb3 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); }, 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..112bebf9ab8d 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -15,13 +15,15 @@ export { } from './utils/tunnel'; export { forwardTunnelRequest as _INTERNAL_forwardTunnelRequest, + handleServedNodeTunnelRequest as _INTERNAL_handleServedNodeTunnelRequest, handleTunnelRequestIfMatched, } from './utils/tunnelForward'; +export { instrumentTunnelServer as _INTERNAL_instrumentTunnelServer } from './utils/tunnelServer'; 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/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/tunnel.ts b/packages/core/src/utils/tunnel.ts index 62144a180134..e4a3fae86393 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; +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,70 @@ 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. + */ +export function getServedTunnelMatch(request: TunnelRequestLike): ServedTunnel | undefined { + const servedTunnel = matchServedTunnel(request); + + 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. */ export function handleServedTunnelRequest(request: Request): Promise | undefined { - const servedTunnel = getServedTunnel(); - if (!servedTunnel?.allowedDsns.length || !matchesTunnel(request, servedTunnel)) { + let servedTunnel: ServedTunnel | undefined | null; + try { + // An envelope always has a body. Nuxt's Cloudflare plugin passes a copy without one, which its app answers later. + servedTunnel = request.body && getServedTunnelMatch(request); + 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..a5f94555ab2e 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,48 @@ export function handleTunnelRequestIfMatched( ); } +/** + * 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. @@ -155,12 +200,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 +217,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. } }, @@ -222,6 +269,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 +306,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/tunnelServer.ts b/packages/core/src/utils/tunnelServer.ts new file mode 100644 index 000000000000..305bdfb3b9dc --- /dev/null +++ b/packages/core/src/utils/tunnelServer.ts @@ -0,0 +1,36 @@ +import { DEBUG_BUILD } from '../debug-build'; +import type { HttpServer } from '../integrations/http/types'; +import { debug } from './debug-logger'; +import { handleServedNodeTunnelRequest } from './tunnelForward'; + +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. + * + * 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?: HttpServer } | undefined)?.server; + if (!server || tunnelServers.has(server)) { + 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: HttpServer, event: string, ...data: unknown[]) { + const [request, response] = data as Parameters; + if (event === 'request' && handleServedNodeTunnelRequest(request, response)) { + return true; + } + + return originalEmit.call(this, event, ...data); + } as HttpServer['emit']; + } catch (error) { + DEBUG_BUILD && debug.error('Sentry tunnel: failed to instrument a server', error); + } +} diff --git a/packages/core/src/utils/tunnelStore.ts b/packages/core/src/utils/tunnelStore.ts new file mode 100644 index 000000000000..1fe317f0c02a --- /dev/null +++ b/packages/core/src/utils/tunnelStore.ts @@ -0,0 +1,62 @@ +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'; + +/** + * 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; +} + +/** + * 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; +} 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/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..d6a4f13898b1 100644 --- a/packages/nitro/src/runtime/plugins/server.ts +++ b/packages/nitro/src/runtime/plugins/server.ts @@ -1,6 +1,6 @@ import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { definePlugin } from 'nitro'; import { useRuntimeConfig } from 'nitro/runtime-config'; @@ -19,7 +19,7 @@ export default definePlugin(nitroApp => { // 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 }); + setTunnelDefaults({ basePath: runtimeConfig.sentryTunnelBasePath || runtimeConfig.app?.baseURL }); const appFetch = nitroApp.fetch; nitroApp.fetch = request => handleServedTunnelRequest(request) ?? appFetch(request); }); 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/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/plugins/tunnel-legacy.server.ts b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts index 89b1a04e8958..da950ea3ea4f 100644 --- a/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts +++ b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts @@ -1,7 +1,7 @@ import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { eventHandler, toWebRequest } from 'h3'; import type { NitroAppPlugin } from 'nitropack'; @@ -14,7 +14,7 @@ import { useRuntimeConfig } from 'nitropack/runtime'; export default (nitroApp => { // The first layer sees the path before Nitro strips `app.baseURL`, so the tunnel is served under it. // `useRuntimeConfig` also picks up a `NUXT_APP_BASE_URL` set at runtime, the same value the browser gets. - setManagedTunnelDefaults({ basePath: useRuntimeConfig().app?.baseURL }); + setTunnelDefaults({ 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. diff --git a/packages/nuxt/src/runtime/plugins/tunnel.server.ts b/packages/nuxt/src/runtime/plugins/tunnel.server.ts index ae4b451edc32..9a63b6fc7d68 100644 --- a/packages/nuxt/src/runtime/plugins/tunnel.server.ts +++ b/packages/nuxt/src/runtime/plugins/tunnel.server.ts @@ -1,6 +1,6 @@ import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { useRuntimeConfig } from 'nitro/runtime-config'; import type { NitroAppPlugin, NitroApp } from 'nitro/types'; @@ -14,7 +14,7 @@ export default ((nitroApp: NitroApp) => { // 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 }); + setTunnelDefaults({ basePath: (useRuntimeConfig() as { app?: { baseURL?: string } }).app?.baseURL }); const appFetch = nitroApp.fetch; nitroApp.fetch = request => handleServedTunnelRequest(request) ?? appFetch(request); }) satisfies NitroAppPlugin; 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/server/tunnelRoute.ts b/packages/react-router/src/server/tunnelRoute.ts index 18b03a6d2c13..7b8541f40d2a 100644 --- a/packages/react-router/src/server/tunnelRoute.ts +++ b/packages/react-router/src/server/tunnelRoute.ts @@ -2,7 +2,7 @@ import { debug, getClient } from '@sentry/core'; import { _INTERNAL_getTunnelPaths as getTunnelPaths, _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -18,7 +18,7 @@ const DISABLED_TUNNEL_PATH = '/__sentry-tunnel-disabled__'; */ export function createSentryTunnelRoutes(basename?: string): Record> { // Runs when the server build loads. Clients created later, e.g. per request on Cloudflare, start with the basename. - setManagedTunnelDefaults({ basePath: basename }); + setTunnelDefaults({ basePath: basename }); // `init` runs from `--import` before the server build loads, so the client and its tunnel paths are usually known. if (!getClient()) { diff --git a/packages/react-router/test/server/tunnelRoute.test.ts b/packages/react-router/test/server/tunnelRoute.test.ts index 38b2a7c7900b..23c5739458a5 100644 --- a/packages/react-router/test/server/tunnelRoute.test.ts +++ b/packages/react-router/test/server/tunnelRoute.test.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/core'; import { createEnvelope, getCurrentScope, getTunnelPath, makeDsn, serializeEnvelope } 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 { createSentryTunnelRoutes } from '../../src/server/tunnelRoute'; @@ -31,7 +31,7 @@ const ENVELOPE = serializeEnvelope(createEnvelope({ dsn: DSN }, [])) as string; describe('createSentryTunnelRoutes', () => { afterEach(() => { getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); vi.unstubAllGlobals(); }); 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): 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..d423c8839fe0 100644 --- a/packages/solidstart/src/nitro/tunnelPlugin.ts +++ b/packages/solidstart/src/nitro/tunnelPlugin.ts @@ -2,7 +2,7 @@ import { GLOBAL_OBJ } from '@sentry/core'; import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest, _INTERNAL_isServedTunnelRequest as isServedTunnelRequest, - _INTERNAL_setManagedTunnelDefaults as setManagedTunnelDefaults, + _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { eventHandler, toWebRequest } from 'h3'; import type { NitroAppPlugin } from 'nitropack'; @@ -15,7 +15,7 @@ import { TUNNEL_BASE_PATH_GLOBAL } from '../common/tunnel'; 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({ + setTunnelDefaults({ basePath: (GLOBAL_OBJ as { [TUNNEL_BASE_PATH_GLOBAL]?: string })[TUNNEL_BASE_PATH_GLOBAL], }); diff --git a/packages/solidstart/test/nitro/tunnelPlugin.test.ts b/packages/solidstart/test/nitro/tunnelPlugin.test.ts index 0f03cef43745..4baea8a5a12f 100644 --- a/packages/solidstart/test/nitro/tunnelPlugin.test.ts +++ b/packages/solidstart/test/nitro/tunnelPlugin.test.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/core'; import { createEnvelope, getCurrentScope, getTunnelPath, makeDsn, serializeEnvelope } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; +import { _INTERNAL_resetTunnelDefaults, 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'; @@ -40,7 +40,7 @@ const onError = vi.fn(); describe('SolidStart v1 tunnel plugin', () => { afterEach(() => { getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); vi.unstubAllGlobals(); vi.clearAllMocks(); }); 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/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/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/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/server/tunnelRoute.ts b/packages/tanstackstart-react/src/server/tunnelRoute.ts index 1d4d09b80553..cd0c4a0e1389 100644 --- a/packages/tanstackstart-react/src/server/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/server/tunnelRoute.ts @@ -5,7 +5,7 @@ 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'; @@ -114,7 +114,7 @@ export function wrapServerEntryWithSentryTunnel( ): 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 }); + setTunnelDefaults({ 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. diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts index cb69c605b918..f91c61e0d2e2 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'; @@ -171,7 +171,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(), diff --git a/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts b/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts index b35224c6e277..7d3fa6d030d3 100644 --- a/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts +++ b/packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts @@ -1,6 +1,6 @@ import type { Client } from '@sentry/core'; import { createEnvelope, getCurrentScope, getTunnelPath, makeDsn, serializeEnvelope } from '@sentry/core'; -import { _INTERNAL_resetManagedTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; +import { _INTERNAL_resetTunnelDefaults, serveTunnelIntegration } from '@sentry/core/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { wrapServerEntryWithSentryTunnel } from '../../src/server/tunnelRoute'; @@ -33,7 +33,7 @@ describe('wrapServerEntryWithSentryTunnel', () => { afterEach(() => { getCurrentScope().setClient(undefined); - _INTERNAL_resetManagedTunnelDefaults(); + _INTERNAL_resetTunnelDefaults(); vi.unstubAllGlobals(); vi.clearAllMocks(); }); diff --git a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts index 2d895e67f249..20008ede700b 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 { @@ -183,7 +183,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. */ From 0da0b0c727a71b439f5b3547f07d444542143aae Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 6 Nov 2026 17:46:06 -0500 Subject: [PATCH 45/92] feat(node,bun,deno): Answer managed tunnel requests in the runtime (#25998) This PR actually delivers the approach outlined in #25997 by hooking up the new utilities and wiring them up to the runtimes appropriately. The implementation introduces a new integration factory that creates an integration for each runtime, it accepts a runtime-specific server patcher, and it applies the HTTP patch itself. ### For Node.js / http module For Node, and other runtimes that emit the http module's diagnostic channel, we patch the server emit and handle the request before it ever hits the user application. ### For Deno / Bun It does the same thing as in Node, but also adds a `*.serve` patch in-case the user is using the native runtime servers. Note for reviewers: if you are lacking context on this PR, check #25997 --- .../node-suites/excludes.ts | 7 + .../suites/deno-serve-managed-tunnel/test.ts | 7 +- .../scripts/consistentExports.ts | 2 + .../managed-tunnel/instrument-late-emit.mjs | 10 + .../managed-tunnel/instrument-no-http.mjs | 14 + .../tracing/managed-tunnel/instrument.mjs | 1 - .../managed-tunnel/server-late-emit.mjs | 20 + .../suites/tracing/managed-tunnel/test.ts | 57 ++- .../suites/tunnel/hardening/instrument.mjs | 9 + .../suites/tunnel/hardening/server.mjs | 24 ++ .../suites/tunnel/hardening/test.ts | 39 ++ dev-packages/test-utils/src/index.ts | 2 + .../test-utils/src/tunnel-hardening.ts | 407 ++++++++++++++++++ packages/bun/src/index.ts | 2 +- packages/bun/src/integrations/serveTunnel.ts | 31 ++ packages/bun/src/tunnelServer.ts | 202 +++++++++ .../test/fixtures/tunnelHardeningServer.ts | 68 +++ packages/bun/test/managedTunnel.test.ts | 66 ++- packages/bun/test/tunnelHardening.test.ts | 107 +++++ .../integrations/http/server-subscription.ts | 11 +- packages/core/src/integrations/http/types.ts | 4 + packages/core/src/integrations/serveTunnel.ts | 4 +- packages/core/src/server.ts | 6 +- packages/core/src/utils/tunnel.ts | 2 +- packages/core/src/utils/tunnelForward.ts | 5 + packages/core/src/utils/tunnelStore.ts | 34 ++ packages/deno/src/index.ts | 2 +- packages/deno/src/integrations/serveTunnel.ts | 31 ++ packages/deno/src/tunnelServer.ts | 65 +++ packages/deno/test/tunnel-hardening.test.ts | 105 +++++ packages/nextjs/src/index.types.ts | 1 + packages/node/src/index.ts | 2 +- packages/node/src/integrations/serveTunnel.ts | 40 ++ packages/server-utils/src/index.ts | 5 + .../src/integrations/serveTunnel.ts | 46 ++ .../src/utils/tunnelServer.ts | 23 +- packages/sveltekit/src/index.types.ts | 1 + .../tanstackstart-react/src/server/sdk.ts | 13 +- 38 files changed, 1446 insertions(+), 29 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-late-emit.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/instrument-no-http.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/managed-tunnel/server-late-emit.mjs create mode 100644 dev-packages/node-integration-tests/suites/tunnel/hardening/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tunnel/hardening/server.mjs create mode 100644 dev-packages/node-integration-tests/suites/tunnel/hardening/test.ts create mode 100644 dev-packages/test-utils/src/tunnel-hardening.ts create mode 100644 packages/bun/src/integrations/serveTunnel.ts create mode 100644 packages/bun/src/tunnelServer.ts create mode 100644 packages/bun/test/fixtures/tunnelHardeningServer.ts create mode 100644 packages/bun/test/tunnelHardening.test.ts create mode 100644 packages/deno/src/integrations/serveTunnel.ts create mode 100644 packages/deno/src/tunnelServer.ts create mode 100644 packages/deno/test/tunnel-hardening.test.ts create mode 100644 packages/node/src/integrations/serveTunnel.ts create mode 100644 packages/server-utils/src/integrations/serveTunnel.ts rename packages/{core => server-utils}/src/utils/tunnelServer.ts (60%) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 86ff4107247b..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, 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/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/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-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 13510989dd0d..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 @@ -94,7 +94,7 @@ describe('managed tunnel', () => { }); 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 => { @@ -105,7 +105,10 @@ 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(); @@ -153,6 +156,56 @@ describe('managed tunnel', () => { }); }); + 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', 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/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/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/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/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index 0cc1f279cdf0..40298235066b 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -25,6 +25,7 @@ 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 { @@ -133,9 +134,14 @@ export function instrumentServer(options: HttpInstrumentationOptions, server: Ht return target.apply(thisArg, args); } - // Tunnel requests record no session or span. The app may still answer them (`intercept: false`), so they keep - // their own isolation scope. + // 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); @@ -251,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 afe614cbcbb3..42a618d48a58 100644 --- a/packages/core/src/integrations/serveTunnel.ts +++ b/packages/core/src/integrations/serveTunnel.ts @@ -19,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/server.ts b/packages/core/src/server.ts index 112bebf9ab8d..1f23709308f7 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -8,17 +8,21 @@ 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, handleTunnelRequestIfMatched, } from './utils/tunnelForward'; -export { instrumentTunnelServer as _INTERNAL_instrumentTunnelServer } from './utils/tunnelServer'; export { serveTunnelIntegration } from './integrations/serveTunnel'; export { _INTERNAL_resetTunnelDefaults, diff --git a/packages/core/src/utils/tunnel.ts b/packages/core/src/utils/tunnel.ts index e4a3fae86393..ec0dea4cdb6c 100644 --- a/packages/core/src/utils/tunnel.ts +++ b/packages/core/src/utils/tunnel.ts @@ -49,7 +49,7 @@ export function setServedTunnel( * 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(client = getClient()): ServedTunnel | undefined { +export function getServedTunnel(client = getClient()): ServedTunnel | undefined { return client && getClientTunnel(client)?.served; } diff --git a/packages/core/src/utils/tunnelForward.ts b/packages/core/src/utils/tunnelForward.ts index a5f94555ab2e..24451dc89f9a 100644 --- a/packages/core/src/utils/tunnelForward.ts +++ b/packages/core/src/utils/tunnelForward.ts @@ -130,6 +130,11 @@ 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. diff --git a/packages/core/src/utils/tunnelStore.ts b/packages/core/src/utils/tunnelStore.ts index 1fe317f0c02a..a6dc118087f9 100644 --- a/packages/core/src/utils/tunnelStore.ts +++ b/packages/core/src/utils/tunnelStore.ts @@ -3,6 +3,7 @@ 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. @@ -23,6 +24,10 @@ export interface ClientTunnelState { 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; } /** @@ -60,3 +65,32 @@ export function getStoredTunnelDefaults(): Readonly | undefined 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/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/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/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/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/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/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/core/src/utils/tunnelServer.ts b/packages/server-utils/src/utils/tunnelServer.ts similarity index 60% rename from packages/core/src/utils/tunnelServer.ts rename to packages/server-utils/src/utils/tunnelServer.ts index 305bdfb3b9dc..6cda5779396f 100644 --- a/packages/core/src/utils/tunnelServer.ts +++ b/packages/server-utils/src/utils/tunnelServer.ts @@ -1,35 +1,40 @@ +import { debug } from '@sentry/core'; +import { + _INTERNAL_handleServedNodeTunnelRequest as handleServedNodeTunnelRequest, + _INTERNAL_httpServerHandlesTunnel as httpServerHandlesTunnel, +} from '@sentry/core/server'; import { DEBUG_BUILD } from '../debug-build'; -import type { HttpServer } from '../integrations/http/types'; -import { debug } from './debug-logger'; -import { handleServedNodeTunnelRequest } from './tunnelForward'; -const tunnelServers = new WeakSet(); +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. + * 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?: HttpServer } | undefined)?.server; - if (!server || tunnelServers.has(server)) { + 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: HttpServer, event: string, ...data: unknown[]) { + 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); - } as HttpServer['emit']; + }; } catch (error) { DEBUG_BUILD && debug.error('Sentry tunnel: failed to instrument a server', error); } 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/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; } From fa21989b30dadb9e1af7a3acb4c20590afa5819c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 6 Nov 2026 17:46:06 -0500 Subject: [PATCH 46/92] feat(cloudflare): Answer managed tunnel requests in `withSentry` (#25999) Adds the tunnel handling logic to our CF workers wrapper, and re-wires astro and other CF capable frameworks. Note for reviewers: if you are lacking context on this PR, check #25997 --- .../cloudflare-integration-tests/runner.ts | 5 ++ .../managed-tunnel/instrument.server.ts | 1 - .../suites/tracing/managed-tunnel/test.ts | 5 +- .../suites/tunnel-hardening/index.ts | 32 ++++++++++++ .../suites/tunnel-hardening/test.ts | 51 +++++++++++++++++++ .../suites/tunnel-hardening/wrangler.jsonc | 6 +++ .../server/api/post-error.ts | 6 +++ .../tests/tunnel-request-body.test.ts | 14 +++++ .../nuxt-5-cloudflare/app/app.vue | 13 +++++ .../nuxt-5-cloudflare/nuxt.config.ts | 18 +++++++ .../nuxt-5-cloudflare/package.json | 41 +++++++++++++++ .../nuxt-5-cloudflare/playwright.config.mjs | 5 ++ .../nuxt-5-cloudflare/pnpm-workspace.yaml | 4 ++ .../nuxt-5-cloudflare/server/api/anchor.ts | 4 ++ .../nuxt-5-cloudflare/server/api/internal.ts | 4 ++ .../server/plugins/sentry.ts | 15 ++++++ .../nuxt-5-cloudflare/start-event-proxy.mjs | 6 +++ .../tests/internal-fetch.test.ts | 26 ++++++++++ .../nuxt-5-cloudflare/tests/tunnel.test.ts | 21 ++++++++ .../nuxt-5-cloudflare/tsconfig.json | 4 ++ .../nuxt-5-cloudflare/wrangler.jsonc | 11 ++++ packages/astro/src/integration/index.ts | 6 ++- .../astro/test/integration/cloudflare.test.ts | 9 ++-- .../src/wrapRequestHandlerWithInit.ts | 16 +++++- packages/core/src/utils/tunnel.ts | 15 +++--- .../plugins/sentry-cloudflare.server.ts | 39 ++++++++++++++ .../src/runtime/utils/event-type-check.ts | 2 + 27 files changed, 361 insertions(+), 18 deletions(-) create mode 100644 dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/index.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/test.ts create mode 100644 dev-packages/cloudflare-integration-tests/suites/tunnel-hardening/wrangler.jsonc create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/server/api/post-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4-cloudflare/tests/tunnel-request-body.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/app/app.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/nuxt.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/pnpm-workspace.yaml create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/anchor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/api/internal.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/server/plugins/sentry.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/internal-fetch.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tests/tunnel.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-5-cloudflare/wrangler.jsonc 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/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/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-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/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 76bae75d5973..0b39040d198b 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -228,7 +228,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/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/cloudflare/src/wrapRequestHandlerWithInit.ts b/packages/cloudflare/src/wrapRequestHandlerWithInit.ts index 1f3ddde1dd6d..ad401f761d1f 100644 --- a/packages/cloudflare/src/wrapRequestHandlerWithInit.ts +++ b/packages/cloudflare/src/wrapRequestHandlerWithInit.ts @@ -2,6 +2,7 @@ import type { CfProperties, IncomingRequestCfProperties } from '@cloudflare/work import { NETWORK_PROTOCOL_NAME, NETWORK_PROTOCOL_VERSION } from '@sentry/conventions/attributes'; import type { SpanAttributes } from '@sentry/core'; import { getIsolationScope, safeCallback } from '@sentry/core'; +import { _INTERNAL_handleServedTunnelRequest as handleServedTunnelRequest } from '@sentry/core/server'; import { wrapServerRequestHandler } from '@sentry/server-utils/request-handler'; import { DEBUG_BUILD } from './debug-build'; import { getHttpServerRequestOptions } from './integrations/httpServer'; @@ -13,9 +14,13 @@ import { addCloudResourceContext, addCultureContext } from './scope-utils'; import { getInvocationState, getInvocationWaitUntil } from './utils/invocationContext'; import { withInvocationIsolationScope } from './utils/invocationScope'; -function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string { +function isDurableObjectContext(context: ExecutionContextCompat | undefined): boolean { // Durable Object fetch handlers use DO state as context (see instrumentDurableObjectWithSentry) - return context && 'storage' in context ? 'auto.faas.cloudflare.durable_object' : 'auto.http.cloudflare'; + return !!context && 'storage' in context; +} + +function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string { + return isDurableObjectContext(context) ? 'auto.faas.cloudflare.durable_object' : 'auto.http.cloudflare'; } export interface RequestHandlerWrapperOptions { @@ -72,6 +77,13 @@ export function wrapRequestHandlerWithInit( // request, where workerd allows publish, and init() above has registered subscribers. flushDeferredChannelEvents(); + // The tunnel answers these before the app, with no span. A Durable Object is only reached through the Worker, + // which answered them already. + const tunnelResponse = !isDurableObjectContext(context) && handleServedTunnelRequest(request, client); + if (tunnelResponse) { + return tunnelResponse.finally(() => waitUntil?.(flushAndDispose(client))); + } + addCloudResourceContext(isolationScope); const attributes: SpanAttributes = {}; if (request.cf) { diff --git a/packages/core/src/utils/tunnel.ts b/packages/core/src/utils/tunnel.ts index ec0dea4cdb6c..1419dce0f648 100644 --- a/packages/core/src/utils/tunnel.ts +++ b/packages/core/src/utils/tunnel.ts @@ -210,10 +210,10 @@ export function isServedTunnelRequest(request: TunnelRequestLike, client?: Clien } /** - * The served tunnel, if the SDK answers this request on it. + * The served tunnel, if the SDK answers this request on it. `client` defaults to the active one. */ -export function getServedTunnelMatch(request: TunnelRequestLike): ServedTunnel | undefined { - const servedTunnel = matchServedTunnel(request); +export function getServedTunnelMatch(request: TunnelRequestLike, client?: Client): ServedTunnel | undefined { + const servedTunnel = matchServedTunnel(request, client); return servedTunnel?.intercept ? servedTunnel : undefined; } @@ -232,13 +232,14 @@ function matchServedTunnel(request: TunnelRequestLike, client?: Client): ServedT * 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. + * 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 { +export function handleServedTunnelRequest(request: Request, client?: Client): Promise | undefined { let servedTunnel: ServedTunnel | undefined | null; try { - // An envelope always has a body. Nuxt's Cloudflare plugin passes a copy without one, which its app answers later. - servedTunnel = request.body && getServedTunnelMatch(request); + // 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; } 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/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 { From c5fed32a9dd818c662ed69e6f638800b108499bb Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 6 Nov 2026 17:46:07 -0500 Subject: [PATCH 47/92] feat(node,aws-serverless): Answer managed tunnel requests on Lambda (#26000) Adds an AWS handler shim that handles tunnel events before they hit the user handler. Note for reviewers: if you are lacking context on this PR, check #25997 --- .../TunnelAsync/index.mjs | 1 + .../TunnelAsync/instrument.mjs | 7 + .../TunnelCallback/index.js | 3 + .../TunnelCallback/instrument.mjs | 7 + .../TunnelStagePath/index.mjs | 1 + .../TunnelStagePath/instrument.mjs | 8 + .../TunnelStreaming/index.mjs | 4 + .../TunnelStreaming/instrument.mjs | 7 + .../TunnelStreamingBrokenAnswer/index.mjs | 4 + .../instrument.mjs | 8 + .../TunnelStreamingUnreachable/index.mjs | 4 + .../TunnelStreamingUnreachable/instrument.mjs | 8 + .../aws-serverless/src/stack.ts | 5 +- .../aws-serverless/tests/tunnel.test.ts | 261 ++++++++++++++++++ .../src/integration/awslambda.ts | 8 +- packages/aws-serverless/src/sdk.ts | 12 + packages/aws-serverless/src/tunnel.ts | 177 ++++++++++++ packages/core/src/server.ts | 1 + packages/core/src/utils/tunnelForward.ts | 24 +- 19 files changed, 537 insertions(+), 13 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/index.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelAsync/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/index.js create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelCallback/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/index.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStagePath/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/index.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreaming/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/index.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingBrokenAnswer/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/index.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TunnelStreamingUnreachable/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/tests/tunnel.test.ts create mode 100644 packages/aws-serverless/src/tunnel.ts 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/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/sdk.ts b/packages/aws-serverless/src/sdk.ts index 539389f4d576..f39077b4d449 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -5,6 +5,7 @@ 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; @@ -192,6 +193,12 @@ export function wrapHandler( : (handler as AsyncHandler>); return async (event: TEvent, context: Context) => { + // Lambda runs no HTTP server, so tunnel requests are answered here, before the handler. + const tunnelResult = handleTunnelEvent(event); + if (tunnelResult) { + return tunnelResult as Promise; + } + context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; timeoutWarningTimer = setupTimeoutWarning(context, options); @@ -244,6 +251,11 @@ function wrapStreamingHandler( responseStream: Parameters>[1], context: Context, ): Promise => { + const tunnelAnswer = handleStreamingTunnelEvent(event, responseStream); + if (tunnelAnswer) { + return tunnelAnswer as Promise; + } + context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; timeoutWarningTimer = setupTimeoutWarning(context, options); 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/core/src/server.ts b/packages/core/src/server.ts index 1f23709308f7..76e802fdfe67 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -21,6 +21,7 @@ export { export { forwardTunnelRequest as _INTERNAL_forwardTunnelRequest, handleServedNodeTunnelRequest as _INTERNAL_handleServedNodeTunnelRequest, + pickForwardedHeaders as _INTERNAL_pickForwardedHeaders, handleTunnelRequestIfMatched, } from './utils/tunnelForward'; export { serveTunnelIntegration } from './integrations/serveTunnel'; diff --git a/packages/core/src/utils/tunnelForward.ts b/packages/core/src/utils/tunnelForward.ts index 24451dc89f9a..9e3298d7e9f9 100644 --- a/packages/core/src/utils/tunnelForward.ts +++ b/packages/core/src/utils/tunnelForward.ts @@ -193,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'; } @@ -247,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)); } From 5b78f8e61e4910e537a8f8ad89b54020433a4c56 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 6 Nov 2026 17:46:07 -0500 Subject: [PATCH 48/92] ref: Remove the framework tunnel hooks (#26001) At this point, we handle the runtimes we support, so the previous framework-specific wiring is no longer needed. This PR adjusts a couple of wiring for some safety around double inits in serverless environments, and cleans up most of the old code introduced in `11.10`. Note for reviewers: if you are lacking context on this PR, check #25997 --- .../tanstackstart-react/tests/tunnel.test.ts | 9 - docs/repeated-init.md | 2 +- packages/astro/src/integration/index.ts | 4 +- packages/astro/src/integration/serverInit.ts | 20 +- packages/astro/src/server/sdk.ts | 12 +- packages/nitro/src/runtime/plugins/server.ts | 14 +- .../runtime/plugins/tunnel-legacy.server.ts | 29 +-- .../nuxt/src/runtime/plugins/tunnel.server.ts | 19 +- packages/nuxt/src/vite/nitroServer.ts | 2 +- packages/react-router/src/cloudflare/index.ts | 2 +- packages/react-router/src/server/index.ts | 2 +- .../react-router/src/server/tunnelRoute.ts | 68 +----- .../src/vite/makeTunnelBasePathPlugin.ts | 28 +++ .../src/vite/makeTunnelRoutePlugin.ts | 30 --- packages/react-router/src/vite/plugin.ts | 4 +- .../test/server/tunnelRoute.test.ts | 90 -------- .../vite/makeTunnelBasePathPlugin.test.ts | 20 ++ .../test/vite/makeTunnelRoutePlugin.test.ts | 28 --- .../react-router/test/vite/plugin.test.ts | 16 +- packages/remix/src/v3/server/instrument.ts | 19 +- packages/remix/test/v3/instrument.test.ts | 30 --- packages/solidstart/src/nitro/tunnelPlugin.ts | 32 +-- .../test/nitro/tunnelPlugin.test.ts | 90 -------- .../tanstackstart-react/src/client/index.ts | 6 +- .../tanstackstart-react/src/index.types.ts | 2 +- .../tanstackstart-react/src/server/index.ts | 2 +- .../src/server/tunnelRoute.ts | 42 +--- .../src/vite/tunnelRoute.ts | 17 +- .../test/server/setTunnelRoute.test.ts | 38 ++++ .../wrapServerEntryWithSentryTunnel.test.ts | 200 ------------------ .../test/vite/tunnelRoute.test.ts | 27 ++- 31 files changed, 182 insertions(+), 722 deletions(-) create mode 100644 packages/react-router/src/vite/makeTunnelBasePathPlugin.ts delete mode 100644 packages/react-router/src/vite/makeTunnelRoutePlugin.ts delete mode 100644 packages/react-router/test/server/tunnelRoute.test.ts create mode 100644 packages/react-router/test/vite/makeTunnelBasePathPlugin.test.ts delete mode 100644 packages/react-router/test/vite/makeTunnelRoutePlugin.test.ts delete mode 100644 packages/solidstart/test/nitro/tunnelPlugin.test.ts create mode 100644 packages/tanstackstart-react/test/server/setTunnelRoute.test.ts delete mode 100644 packages/tanstackstart-react/test/server/wrapServerEntryWithSentryTunnel.test.ts 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/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/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 0b39040d198b..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); } 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/server/sdk.ts b/packages/astro/src/server/sdk.ts index 98910a1db2fd..98fc2d44e198 100644 --- a/packages/astro/src/server/sdk.ts +++ b/packages/astro/src/server/sdk.ts @@ -1,8 +1,9 @@ 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 { DEBUG_BUILD } from '../debug-build'; declare const EdgeRuntime: string | undefined; declare const Deno: 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, }; diff --git a/packages/nitro/src/runtime/plugins/server.ts b/packages/nitro/src/runtime/plugins/server.ts index d6a4f13898b1..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_setTunnelDefaults as setTunnelDefaults, -} 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 }; setTunnelDefaults({ basePath: runtimeConfig.sentryTunnelBasePath || runtimeConfig.app?.baseURL }); - const appFetch = nitroApp.fetch; - nitroApp.fetch = request => handleServedTunnelRequest(request) ?? appFetch(request); }); diff --git a/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts index da950ea3ea4f..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_setTunnelDefaults as setTunnelDefaults, -} 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. setTunnelDefaults({ 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)); - }), - }); }) satisfies NitroAppPlugin; diff --git a/packages/nuxt/src/runtime/plugins/tunnel.server.ts b/packages/nuxt/src/runtime/plugins/tunnel.server.ts index 9a63b6fc7d68..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_setTunnelDefaults as setTunnelDefaults, -} 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. +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 }); - const appFetch = nitroApp.fetch; - nitroApp.fetch = request => handleServedTunnelRequest(request) ?? appFetch(request); }) satisfies NitroAppPlugin; 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/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 7b8541f40d2a..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_setTunnelDefaults as setTunnelDefaults, -} 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. setTunnelDefaults({ 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' } }), - }, - }; } 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 23c5739458a5..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_resetTunnelDefaults, 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_resetTunnelDefaults(); - 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/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/solidstart/src/nitro/tunnelPlugin.ts b/packages/solidstart/src/nitro/tunnelPlugin.ts index d423c8839fe0..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_setTunnelDefaults as setTunnelDefaults, -} 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. +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/nitro/tunnelPlugin.test.ts b/packages/solidstart/test/nitro/tunnelPlugin.test.ts deleted file mode 100644 index 4baea8a5a12f..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_resetTunnelDefaults, 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_resetTunnelDefaults(); - 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/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/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/tunnelRoute.ts b/packages/tanstackstart-react/src/server/tunnelRoute.ts index cd0c4a0e1389..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_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. +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 }); - - // 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; } diff --git a/packages/tanstackstart-react/src/vite/tunnelRoute.ts b/packages/tanstackstart-react/src/vite/tunnelRoute.ts index f91c61e0d2e2..cc280e24b514 100644 --- a/packages/tanstackstart-react/src/vite/tunnelRoute.ts +++ b/packages/tanstackstart-react/src/vite/tunnelRoute.ts @@ -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, @@ -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 7d3fa6d030d3..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_resetTunnelDefaults, 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_resetTunnelDefaults(); - 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/tunnelRoute.test.ts b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts index 20008ede700b..110b1ccae40b 100644 --- a/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts +++ b/packages/tanstackstart-react/test/vite/tunnelRoute.test.ts @@ -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 }); }); From 6c6c040256450996682d7b73b3f2693cc4bfbb69 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:49:58 +0000 Subject: [PATCH 49/92] fix(deps): Bump the sentry CLI to ^0.49.0 (#26200) **Why:** Before `sentry` 0.49.0, every `createSentrySDK()` call opened and wrote the CLI's local `cli.db`. When a build runs on a different Node version than the one that created that file (for example through `npx sentry`), the open fails with `SQLite3Error: unable to open database file`. The build still passes, but release creation and source map uploads fail silently. In 0.49.0, SDK calls write no local state by default (getsentry/toolkit#2074) and fall back to running without `cli.db` (getsentry/toolkit#2069, getsentry/toolkit#2070). This bumps `@sentry/bundler-plugins`, `@sentry/remix` and `@sentry/react-router`, plus the bundler plugin integration tests so the workspace resolves a single version. None of them set `persist`, so the new default applies as-is. **How to review:** The four `package.json` ranges and the lockfile. 0.49.0 was published today, so pnpm added it to `minimumReleaseAgeExclude`, as with the earlier first-party `@sentry/*` entries there. **Unsure about:** The `cloudflare-*-send-to-sentry` e2e apps pin `sentry@~0.44.1` on their own. I left them alone because they're separate test apps and don't go through the bundler plugin. Closes #26197 via @BYK. -- [View Session in Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A87622ce3671938950bc6e630/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com> --- .../package.json | 2 +- packages/bundler-plugins/package.json | 2 +- packages/react-router/package.json | 2 +- packages/remix/package.json | 2 +- pnpm-lock.yaml | 29 +++++++------------ pnpm-workspace.yaml | 1 + 6 files changed, 16 insertions(+), 22 deletions(-) diff --git a/dev-packages/bundler-plugin-integration-tests/package.json b/dev-packages/bundler-plugin-integration-tests/package.json index 6fdddbb398bb..b255ad7b19a8 100644 --- a/dev-packages/bundler-plugin-integration-tests/package.json +++ b/dev-packages/bundler-plugin-integration-tests/package.json @@ -14,7 +14,7 @@ "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" } diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index 4a1481196d1f..c0df6df7697d 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -120,7 +120,7 @@ "glob": "^14.0.3", "magic-string": "~0.30.8 || ^1.0.0", "oxc-parser": "^0.152.0", - "sentry": "^0.45.0", + "sentry": "^0.49.0", "supports-color": "^8.1.1" }, "peerDependencies": { diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 6a2ffbf51eb0..3bfee7bc2b05 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -64,7 +64,7 @@ "@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", diff --git a/packages/remix/package.json b/packages/remix/package.json index 428b85e0a300..9cdd45d0e684 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -84,7 +84,7 @@ "@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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e5e575b931a..cb7c360da785 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,8 +394,8 @@ importers: 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 @@ -1165,8 +1165,8 @@ importers: specifier: ^0.152.0 version: 0.152.0 sentry: - specifier: ^0.45.0 - version: 0.45.0 + specifier: ^0.49.0 + version: 0.49.0 supports-color: specifier: ^8.1.1 version: 8.1.1 @@ -1974,8 +1974,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 devDependencies: '@react-router/dev': specifier: ^7.18.4 @@ -2035,8 +2035,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 @@ -17133,13 +17133,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 @@ -38766,9 +38761,7 @@ snapshots: transitivePeerDependencies: - supports-color - sentry@0.44.0: {} - - sentry@0.45.0: {} + sentry@0.49.0: {} serialize-javascript@6.0.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 741d003eefef..12b1a927e453 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -126,3 +126,4 @@ minimumReleaseAgeExclude: - '@sentry/rrweb-snapshot@2.45.0' - '@sentry/rrweb-types@2.45.0' - '@sentry/rrweb@2.45.0' + - sentry@0.49.0 From fe412ef0975438627fd2807f54e683183b312d6c Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 9 Nov 2026 09:21:51 +0100 Subject: [PATCH 50/92] test(aws-serverless): Add Bedrock streaming integration scenario (#26190) ## What Adds a Bedrock streaming integration scenario that checks the current span behavior for ConverseStream and InvokeModelWithResponseStream streams that finish, fail, or are aborted. ## Why Before this change, no integration test covered Bedrock streams. This scenario is the base for the input and output recording PRs. Extracted from #26118. --------- Co-authored-by: Claude Opus 5.5 --- .../node-integration-tests/package.json | 1 + .../bedrock/scenario-stream.mjs | 200 ++++++++++++++++++ .../suites/aws-serverless/bedrock/test.ts | 30 +++ pnpm-lock.yaml | 3 + 4 files changed, 234 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/bedrock/scenario-stream.mjs diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index e801ce94c331..edb41d914cc7 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -68,6 +68,7 @@ "@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", 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 de0d7878c826..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'; @@ -52,6 +53,29 @@ function assertBedrockSpans(container: SerializedStreamedSpanContainer): void { 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', () => { afterAll(() => { cleanupChildProcesses(); @@ -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/pnpm-lock.yaml b/pnpm-lock.yaml index cb7c360da785..7fa92962de27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -720,6 +720,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 From 7259f5d9b52e7b7ec964ddb2d297ad628d2a2ad5 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette <47537704+arthurfiorette@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:27:11 -0300 Subject: [PATCH 51/92] feat(cloudflare): propagate tracing context through Queue messages (#25306) This extends #22303, which was auto-closed for inactivity after its review comments slipped through my GitHub inbox. It is now synced with `develop` and addresses that review. --- Adds opt-in trace propagation from Cloudflare Queue producers to consumers. * When a producer binding matches `queueTracePropagationBindings`, plain-object message bodies carry the producer's `sentry-trace` under `__sentry_queue_trace__`. * Arrays, binary payloads and other non-plain values are sent unchanged. * Consumers remove that field before invoking the application handler. * `queue.process` remains a root span and links to every distinct producer in the batch. Span links are used instead of continuing a producer trace because a Queue batch can contain messages from unrelated traces. Picking one producer as the parent would create false causal relationships for the other messages. Propagation is opt-in per binding because Cloudflare Queues has no message-header API. The SDK has to modify the message body, so consumers without Sentry would see the extra field. Changes since #22303: * `enableQueueTracePropagation: boolean` became `queueTracePropagationBindings`, mirroring `rpcTracePropagationBindings` (#23486). Only the producer opts in, since it is the side that changes the payload. Consumers always strip and link, the same way RPC receivers handle `__sentry_rpc_meta__`. * Links no longer set `sentry.link.type: previous_trace`. A batch fans in producers rather than forming a previous/next chain. Fixes #22309 --------- Co-authored-by: Claude Opus 5.5 --- .../suites/queue/index.ts | 6 + .../suites/queue/test.ts | 39 +++- packages/cloudflare/src/client.ts | 31 ++++ .../instrumentations/worker/instrumentEnv.ts | 7 +- .../worker/instrumentQueue.ts | 39 ++++ .../worker/instrumentQueueProducer.ts | 67 +++++-- ...pcPropagation.ts => bindingPropagation.ts} | 25 ++- .../cloudflare/src/utils/queueTraceMeta.ts | 63 +++++++ .../instrumentations/instrumentEnv.test.ts | 28 +++ .../worker/instrumentQueue.test.ts | 103 +++++++++++ .../worker/instrumentQueueProducer.test.ts | 169 ++++++++++++++++++ ...ion.test.ts => bindingPropagation.test.ts} | 2 +- 12 files changed, 546 insertions(+), 33 deletions(-) rename packages/cloudflare/src/utils/{rpcPropagation.ts => bindingPropagation.ts} (65%) create mode 100644 packages/cloudflare/src/utils/queueTraceMeta.ts rename packages/cloudflare/test/utils/{rpcPropagation.test.ts => bindingPropagation.test.ts} (96%) 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/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/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/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', () => { From b80191e7529687c9a2db6ce19393e5036ebeb90a Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:39:53 +0100 Subject: [PATCH 52/92] chore: Add external contributor to CHANGELOG.md (#26214) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #25306 Co-authored-by: JPeer264 <10677263+JPeer264@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b899ad5b4f03..d7e84c8acbf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg, @rjmcgee53192, and @adityankale190895. Thank you for your contributions! +Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, and @arthurfiorette. Thank you for your contributions! ### Important Changes From cfcb5c9447d36658b4b208ebba87bd066b49f4df Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Nov 2026 12:14:56 +0000 Subject: [PATCH 53/92] feat(bundler-plugins): Enable hidden source maps and remove them after upload in the webpack plugin (#25647) Part of the stack for getsentry/sentry-javascript#25609. When `devtool` is unset and an upload can run, the plugin sets `hidden-source-map`; a visible devtool with `deleteSourcemapsAfterUpload: true` gets the `hidden-` prefix, and a plain `eval` devtool counts as source maps turned off. The maps are staged at the end of `processAssets`, after the real content hashes are in place, and deleted from the compilation together with any compressed variants, so webpack never emits them. When the plugin turned the maps on, every map goes, including CSS maps and the maps of chunks that `sourcemaps.assets` or `sourcemaps.ignore` exclude; maps the user enabled stay unless their chunk was uploaded. Development builds and a `devtool: false` with a manual `SourceMapDevToolPlugin` stay untouched. With webpack's array form of `devtool`, `true` replaces the array with one hidden setting for all asset types, because the compatibility proxy hides the array from plugins; the option docs say so. Rspack uses the same hooks but is not covered by tests, because `@rspack/core` is not installed in the repo. Integration fixtures cover `ignore` and `assets` on the staged path for webpack 5. --- .../fixtures/webpack5/basic-cjs.test.ts | 1 + .../fixtures/webpack5/basic.test.ts | 1 + .../webpack5/release-disabled.test.ts | 1 + .../webpack5/sourcemaps-assets-auto.config.js | 20 + .../webpack5/sourcemaps-assets-auto.test.ts | 31 ++ .../webpack5/sourcemaps-ignore-auto.config.js | 20 + .../webpack5/sourcemaps-ignore-auto.test.ts | 31 ++ packages/bundler-plugins/src/core/types.ts | 3 + packages/bundler-plugins/src/webpack/index.ts | 173 ++++++- .../delete-sourcemaps-after-upload.test.ts | 463 ++++++++++++++++++ .../buildTimeOptionsBase.ts | 3 + 11 files changed, 745 insertions(+), 2 deletions(-) create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets-auto.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore-auto.test.ts create mode 100644 packages/bundler-plugins/test/webpack/delete-sourcemaps-after-upload.test.ts 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-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/packages/bundler-plugins/src/core/types.ts b/packages/bundler-plugins/src/core/types.ts index 28de6c6ee95e..95fca5f937fb 100644 --- a/packages/bundler-plugins/src/core/types.ts +++ b/packages/bundler-plugins/src/core/types.ts @@ -204,6 +204,9 @@ export interface Options { * `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/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/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/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index 13e9346243fb..1bf9505714f1 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -260,6 +260,9 @@ interface SourceMapsOptions { * 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; From 34326ef52048536aa2b4c82c6b529bf41816d620 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 15:39:53 +0100 Subject: [PATCH 54/92] test(browser): Trigger third-party errors after initialization (#26206) **Why:** The third-party script could throw on its 100 ms timer before the deferred SDK bundle initialized, leaving the test waiting for an event that was never captured. Triggering it from a click after page load preserves the third-party stack while removing the initialization race. Closes #26203 Co-authored-by: GPT-6 --- .../suites/integrations/thirdPartyErrorsFilter/template.html | 1 + .../suites/integrations/thirdPartyErrorsFilter/test.ts | 1 + .../integrations/thirdPartyErrorsFilter/thirdPartyScript.js | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) 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); +}); From cac83d96f392cddeb05a2e92a8d36c64d156c863 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 15:41:20 +0100 Subject: [PATCH 55/92] test(hono): relax middleware span timing assertions (#26199) **Why:** Allow 5ms of timer variance in Hono middleware span tests to avoid CI flakes while preserving duration checks. --------- Co-authored-by: OpenAI Codex --- .../hono-4-legacy/tests/middleware.test.ts | 8 ++++---- .../test-applications/hono-4/tests/middleware.test.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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..6af9362b0991 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 @@ -49,7 +49,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 +138,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 }) => { 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..6af9362b0991 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 @@ -49,7 +49,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 +138,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 }) => { From f27fc733d94f2a8a746a522896676a6460b9a1ee Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 15:48:06 +0100 Subject: [PATCH 56/92] test(cloudflare): Poll Workflow send aborts in the run context (#26207) Applying https://github.com/getsentry/sentry-javascript/pull/25182 also to develop. --------- Co-authored-by: GPT-6 --- .../suites/flush-timeout/index.ts | 8 ++++++-- .../suites/flush-timeout/instrument.server.ts | 1 - .../suites/flush-timeout/lastSend.ts | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) 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 }; From 323de1f2b1cb129c4f2935a46f08f1e5b45cf3bc Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 15:50:08 +0100 Subject: [PATCH 57/92] test(cloudflare): tolerate span timestamp rounding (#26196) **Why:** Allow 1ms tolerance in the streamed-response span timing assertion to avoid rounding-related flakes while still checking that the parent span covers rendering. --------- Co-authored-by: OpenAI Codex --- .../suites/request-handler/streamed-response/test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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); }, ); From e342629c60a97b90b1b0358d30390783cf524831 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Nov 2026 14:54:25 +0000 Subject: [PATCH 58/92] test(e2e): Pin elysia to v1 in the elysia-node test app (#26215) The elysia-node E2E app installs `elysia@latest`, which now resolves to `2.0.0-rc.4`. `@elysiajs/node@1.4.5` imports `elysia/cookies`, which that version no longer exports, so the web server fails to start and the required `E2E elysia-node Test` job fails on develop and every open PR. The other two elysia apps and the `@sentry/elysia` peer dependency already use `^1.4.0`. One line in `dev-packages/e2e-tests/test-applications/elysia-node/package.json`. Supporting Elysia 2 is a separate question for the SDK, not for this test pin. --- .../e2e-tests/test-applications/elysia-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": { From c4fbc0a2cc36746001963fb83c98f93e0c12a84c Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:11:46 +0100 Subject: [PATCH 59/92] fix(aws-serverless): Fork isolation scope per invocation (#26191) ## What `wrapHandler` now forks the isolation scope for each invocation, for normal and streaming handlers. The timeout warning now has the user and `aws.lambda` context of its own invocation. ## Why A warm Lambda kept one isolation scope, so the user, tags, and breadcrumbs of one invocation went to the events of later invocations. Closes: #26180 --------- Co-authored-by: Claude Opus 5.5 --- .../IsolationScope/index.js | 9 ++ .../aws-serverless-layer/tests/layer.test.ts | 27 ++++++ packages/aws-serverless/src/sdk.ts | 16 ++-- .../aws-serverless/test/isolation.test.ts | 87 +++++++++++++++++++ packages/aws-serverless/test/sdk.test.ts | 9 +- 5 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/IsolationScope/index.js create mode 100644 packages/aws-serverless/test/isolation.test.ts 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/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index f39077b4d449..d5b8fdaa7624 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -1,6 +1,6 @@ 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'; @@ -165,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 @@ -201,9 +199,8 @@ export function wrapHandler( context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; - timeoutWarningTimer = setupTimeoutWarning(context, options); - async function processResult(): Promise { + const timeoutWarningTimer = setupTimeoutWarning(context, options); const scope = getCurrentScope(); let rv: TResult; @@ -233,7 +230,7 @@ export function wrapHandler( return rv; } - return withScope(async () => { + return withIsolationScope(async () => { return processResult(); }); }; @@ -244,8 +241,6 @@ function wrapStreamingHandler( options: WrapperOptions, startTime: number, ): StreamifyHandler { - let timeoutWarningTimer: NodeJS.Timeout | undefined; - const wrappedHandler = async ( event: TEvent, responseStream: Parameters>[1], @@ -258,9 +253,8 @@ function wrapStreamingHandler( context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop; - timeoutWarningTimer = setupTimeoutWarning(context, options); - async function processStreamingResult(): Promise { + const timeoutWarningTimer = setupTimeoutWarning(context, options); const scope = getCurrentScope(); try { @@ -286,7 +280,7 @@ function wrapStreamingHandler( } } - return withScope(() => processStreamingResult()); + return withIsolationScope(() => processStreamingResult()); }; const handlerWithSymbols = handler as unknown as Record; 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/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'); }); From 33522c0a849c031e6dc5142dd3dffdc825dc3096 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 16:21:46 +0100 Subject: [PATCH 60/92] perf(ci): combine circular dependency checks across entrypoints (#26195) **Why:** Packages with multiple entrypoints currently start Madge separately for each one, repeatedly parsing shared dependencies. Combining entrypoints reduces invocations from 61 to 35 across 13 packages. Time savings are only a few seconds for the lint job, but no need to do unnecessary work. --------- Co-authored-by: OpenAI Codex --- packages/astro/package.json | 2 +- packages/effect/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/react-router/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/vue/package.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index eda3fbd46261..36536ad52e16 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -79,7 +79,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-astro-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/effect/package.json b/packages/effect/package.json index 35ecd497d664..e01e235dd387 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -71,7 +71,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-effect-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index b82a41e601d6..55d05b806a3b 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -81,7 +81,7 @@ "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", + "circularDepCheck": "madge --circular src/index.ts src/setup.ts", "clean": "rimraf build coverage sentry-nestjs-*.tgz ./*.d.ts ./*.d.ts.map", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index fed113b51450..636e172e62f3 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -117,7 +117,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/edge/index.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-nextjs-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index cd9e1508ce4b..e26a57c5df94 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -89,7 +89,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-nuxt-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 3bfee7bc2b05..23044959e20b 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -92,7 +92,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-react-router-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/react/package.json b/packages/react/package.json index 488021aba4e7..ea99a7c70196 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -100,7 +100,7 @@ "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", + "circularDepCheck": "madge --circular src/index.ts src/react-router.ts", "clean": "rimraf build coverage sentry-react-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/remix/package.json b/packages/remix/package.json index 9cdd45d0e684..c96cbbe21295 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -117,7 +117,7 @@ "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", + "circularDepCheck": "madge --circular src/index.server.ts src/index.client.ts", "clean": "rimraf build coverage sentry-remix-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/solid/package.json b/packages/solid/package.json index c18088cb9dce..5321e5db64f5 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -94,7 +94,7 @@ "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", + "circularDepCheck": "madge --circular src/index.ts src/solidrouter.ts src/tanstackrouter.ts", "clean": "rimraf build coverage sentry-solid-*.tgz ./*.d.ts ./*.d.ts.map", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 021946afc6eb..e42e239068ef 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -117,7 +117,7 @@ "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", + "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": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 1760c9d60ab0..cbf1fe206520 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -102,7 +102,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-sveltekit-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index dfe319e84304..a7e846f01cc8 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -75,7 +75,7 @@ "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", + "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-tanstackstart-react-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/vue/package.json b/packages/vue/package.json index 5bcea0e741dd..fba6e037b9a0 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -77,7 +77,7 @@ "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", + "circularDepCheck": "madge --circular src/index.ts src/tanstackrouter.ts", "clean": "rimraf build coverage sentry-vue-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", From 356d472c6ab0743314c861a97407555eb5c37ab6 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 16:27:04 +0100 Subject: [PATCH 61/92] test(browser): Use navigation-aware back actions in bfcache tests (#26208) **Why:** Calling history.back() through page.evaluate can lose its execution context as navigation starts. Use Playwright's navigation-aware back action, waiting only for commit because bfcache restores do not fire another load event. Co-authored-by: GPT-6 --- .../browser-bfcache/tests/bfcache.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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, { From 4316c2634a5f3de43cbd63780117d9221a23e3cd Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 9 Nov 2026 16:44:05 +0100 Subject: [PATCH 62/92] perf(ci): Use pnpm pack for tarballs (#26024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Tarball preparation delays every E2E app. Switching from `npm pack` to `pnpm pack` with four workers made tarballs available to E2E jobs about two minutes earlier in our benchmark. | Metric | Before: npm, 8 workers | After: pnpm, 4 workers | Improvement | |---|---:|---:|---:| | Packing step | 2m17s | **11s** | **92% faster** | | Whole tarball job | 3m01s | **1m02s** | **66% faster** | | Tarballs ready, excluding initial queueing | 5m01s | **3m04s** | **1m57s earlier** | **Unsure about:** There are some minor changes to the packaged tarballs, but all of these probably don't matter iiuc or might even be improvements (e.g. including licenses in packages where they were missing atm). List of tarball changes courtesy of codex: - `repository` changes from a URL string to a `{ type, url }` object. - Manifest fields are reordered, and `build:tarball` now references pnpm. - Ember’s published manifest drops `prepack`; the build hook still runs before packing. - Four packages gain the workspace LICENSE: server-utils, runtime-injection, Ember, and internal test-utils. - Runtime code, types, exports, dependencies, filenames, and permissions are preserved, apart from normal build commit identifiers. --------- Co-authored-by: OpenAI Codex --- .github/workflows/build.yml | 2 +- dev-packages/test-utils/package.json | 2 +- packages/angular/package.json | 2 +- packages/astro/package.json | 2 +- packages/aws-serverless/package.json | 2 +- packages/browser-utils/package.json | 2 +- packages/browser/package.json | 2 +- packages/bun/package.json | 2 +- packages/bundler-plugins/package.json | 2 +- packages/cloudflare/package.json | 2 +- packages/core/package.json | 2 +- packages/deno/package.json | 2 +- packages/effect/package.json | 2 +- packages/elysia/package.json | 2 +- packages/ember/package.json | 2 +- packages/esbuild-plugin/package.json | 2 +- packages/eslint-config-sdk/package.json | 2 +- packages/eslint-plugin-sdk/package.json | 2 +- packages/feedback/package.json | 2 +- packages/gatsby/package.json | 2 +- packages/google-cloud-serverless/package.json | 2 +- packages/hono/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nitro/package.json | 2 +- packages/node-native/package.json | 2 +- packages/node/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/opentelemetry/package.json | 2 +- packages/profiling-node/package.json | 2 +- packages/react-router/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/replay-canvas/package.json | 2 +- packages/replay-internal/package.json | 2 +- packages/rollup-plugin/package.json | 2 +- packages/server-runtime-injection/package.json | 2 +- packages/server-utils/package.json | 2 +- packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/typescript/package.json | 2 +- packages/vercel-edge/package.json | 2 +- packages/vite-plugin/package.json | 2 +- packages/vue/package.json | 2 +- packages/wasm/package.json | 2 +- packages/webpack-plugin/package.json | 2 +- 49 files changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3138bc70bcec..59211cdc3d6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1058,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 diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 5105dc3dd7b4..d50ce05a313a 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -51,7 +51,7 @@ "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", diff --git a/packages/angular/package.json b/packages/angular/package.json index 360f2baaffa1..56e66857d5f9 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -49,7 +49,7 @@ "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", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/astro/package.json b/packages/astro/package.json index 36536ad52e16..61615e11416c 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -78,7 +78,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", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-astro-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 66fd00940a84..29fa356d6fea 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -89,7 +89,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build dist-awslambda-layer coverage sentry-serverless-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 7c88091ad10f..e1a654eea647 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -48,7 +48,7 @@ "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", + "build:tarball": "pnpm pack", "clean": "rimraf build coverage sentry-browser-utils-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/browser/package.json b/packages/browser/package.json index 75315a5477dd..995f4daddab0 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -60,7 +60,7 @@ "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", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/bun/package.json b/packages/bun/package.json index 7cbd909d7b53..dfedfbbb6727 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -61,7 +61,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-bun-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index c0df6df7697d..8c4d660899a9 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", diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index d6a219cd9bdd..0800f7843b63 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -91,7 +91,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-cloudflare-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/core/package.json b/packages/core/package.json index e434a7151013..f0a2b16e9714 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -75,7 +75,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-core-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/deno/package.json b/packages/deno/package.json index d96b09353a3b..9dcd96de03e4 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -42,7 +42,7 @@ "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", "prefix": "pnpm run deno-types", diff --git a/packages/effect/package.json b/packages/effect/package.json index e01e235dd387..43c22246785f 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -70,7 +70,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", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-effect-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/elysia/package.json b/packages/elysia/package.json index 084561c8869c..1be217b97693 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -57,7 +57,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-elysia-*.tgz", "lint": "oxlint . --type-aware", diff --git a/packages/ember/package.json b/packages/ember/package.json index b4c15359e379..ca68ba70f8af 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -28,7 +28,7 @@ "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", diff --git a/packages/esbuild-plugin/package.json b/packages/esbuild-plugin/package.json index 882c2bb67e9e..93028b0103a0 100644 --- a/packages/esbuild-plugin/package.json +++ b/packages/esbuild-plugin/package.json @@ -40,7 +40,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", "clean": "rimraf build coverage sentry-esbuild-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 15510c3fe989..c6eb7e52bb39 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -40,7 +40,7 @@ }, "scripts": { "clean": "pnpm exec rimraf sentry-eslint-config-sdk-*.tgz", - "build:tarball": "npm pack", + "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..546a110abcf5 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -32,7 +32,7 @@ "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..3a7c9e2353ec 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -49,7 +49,7 @@ "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", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index e60d029a1945..0b131facda3b 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -63,7 +63,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage *.d.ts sentry-gatsby-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 1970592e6c6c..9db4358d1aef 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -66,7 +66,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-google-cloud-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/hono/package.json b/packages/hono/package.json index f59c42626665..2fbb3948dbc9 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -122,7 +122,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-hono-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 55d05b806a3b..60b2d8e0224b 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/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", "circularDepCheck": "madge --circular src/index.ts src/setup.ts", "clean": "rimraf build coverage sentry-nestjs-*.tgz ./*.d.ts ./*.d.ts.map", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 636e172e62f3..c078bb3b5aa4 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -116,7 +116,7 @@ "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", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/edge/index.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-nextjs-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/nitro/package.json b/packages/nitro/package.json index e1a869e5ee75..817182417c09 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -59,7 +59,7 @@ "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", + "build:tarball": "pnpm pack", "clean": "rimraf build coverage sentry-nitro-*.tgz", "lint": "oxlint . --type-aware", "lint:types": "oxlint src --type-aware --type-check", 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 acf5a4cb0565..5f299a4ea431 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -97,7 +97,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-node-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index e26a57c5df94..798f15998c02 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -88,7 +88,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", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-nuxt-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 4b9f10734ceb..c7ad5404b17f 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -45,7 +45,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-opentelemetry-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 0c0dd47a7b73..1e472549e13a 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -44,7 +44,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", "test:bundle": "node test-binaries.esbuild.js", "test": "vitest run", "test:watch": "vitest --watch" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 23044959e20b..229a552dbf78 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -91,7 +91,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", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-react-router-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/react/package.json b/packages/react/package.json index ea99a7c70196..209ffa946e17 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -99,7 +99,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", "circularDepCheck": "madge --circular src/index.ts src/react-router.ts", "clean": "rimraf build coverage sentry-react-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/remix/package.json b/packages/remix/package.json index c96cbbe21295..83340efe196b 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -116,7 +116,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", "circularDepCheck": "madge --circular src/index.server.ts src/index.client.ts", "clean": "rimraf build coverage sentry-remix-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index df3f45a26c59..5c8f8cc28aba 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -33,7 +33,7 @@ "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", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 051253e150bb..989521a888d2 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -47,7 +47,7 @@ "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", "format": "oxfmt \"src/**/*.ts\" \"test/**/*.ts\" --write", diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index d10f6573cdba..68b1a58eb913 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -41,7 +41,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", "clean": "rimraf build coverage sentry-rollup-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/server-runtime-injection/package.json b/packages/server-runtime-injection/package.json index 059ef4239f8c..34f71da49740 100644 --- a/packages/server-runtime-injection/package.json +++ b/packages/server-runtime-injection/package.json @@ -65,7 +65,7 @@ "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", + "build:tarball": "pnpm pack", "clean": "rimraf build coverage sentry-server-runtime-injection-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 8386c81a0ee2..511b18ab3e29 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -122,7 +122,7 @@ "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", + "build:tarball": "pnpm pack", "clean": "rimraf build coverage sentry-server-utils-*.tgz", "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", diff --git a/packages/solid/package.json b/packages/solid/package.json index 5321e5db64f5..555469540092 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -93,7 +93,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", "circularDepCheck": "madge --circular src/index.ts src/solidrouter.ts src/tanstackrouter.ts", "clean": "rimraf build coverage sentry-solid-*.tgz ./*.d.ts ./*.d.ts.map", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index e42e239068ef..d450cb852aa4 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -116,7 +116,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", "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": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2c1e323bc190..8d10df92a5c9 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -55,7 +55,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-svelte-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index cbf1fe206520..cc07dca47c54 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -101,7 +101,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", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-sveltekit-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index a7e846f01cc8..43c36aa8cfe9 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -74,7 +74,7 @@ "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", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", "clean": "rimraf build coverage sentry-tanstackstart-react-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 3afe8c68ed3b..dd20991b732e 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -17,6 +17,6 @@ }, "scripts": { "clean": "pnpm exec rimraf sentry-typescript-*.tgz", - "build:tarball": "npm pack" + "build:tarball": "pnpm pack" } } diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 9d9e62bd1574..2348a9c34b89 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -49,7 +49,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", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-vercel-edge-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 331191e40d40..78287002881a 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -40,7 +40,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", "clean": "rimraf build coverage sentry-vite-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/vue/package.json b/packages/vue/package.json index fba6e037b9a0..c88b9e03fa87 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -76,7 +76,7 @@ "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", + "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts src/tanstackrouter.ts", "clean": "rimraf build coverage sentry-vue-*.tgz", "lint:fix": "oxlint . --fix --type-aware", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 9a35d69429dc..4534be38c341 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -48,7 +48,7 @@ "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", diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 3579068d871d..090be0093a7b 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -45,7 +45,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", "clean": "rimraf build coverage sentry-webpack-plugin-*.tgz", "lint": "oxlint . --type-aware", "lint:fix": "oxlint . --fix --type-aware", From c9ef0cb1cccea7c527ec4bb987824a152f091232 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 10 Nov 2026 06:46:55 +0100 Subject: [PATCH 63/92] chore(deps): Bump path-to-regexp to fix CVE-2026-4867 (#26174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Fixes Dependabot alert [3189](https://github.com/getsentry/sentry-javascript/security/dependabot/3189) (path-to-regexp ReDoS, high). Bumps path-to-regexp 0.1.12 β†’ 0.1.13, which is within the `~0.1.12` range of express 4, so it only touches the lockfile. Dev/test-only (express in integration tests and test-utils). Co-authored-by: Claude Opus 5.5 --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fa92962de27..5ef0d679ae82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15633,8 +15633,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==} @@ -32007,7 +32007,7 @@ 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 range-parser: 1.2.1 @@ -36877,7 +36877,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: From 840cfd02bde3598f2adfec7d4182c2a54c934235 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:14:50 +0100 Subject: [PATCH 64/92] test(hono): Fix flaky middleware error status test (#26213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** `root app middleware β€Ί sets error status on middleware span when middleware throws` failed in 2 `E2E hono-4 (cloudflare) Test` runs on `develop` last week ([weekly report](https://github.com/getsentry/sentry-javascript/actions/runs/42572055999)). In both runs the segment was found, but no middleware span with `status: 'error'` was. The test took the first `GET /error` segment that arrived. The test before it requests the same route but only waits for the error event, so that request's segment can arrive late while this test is listening. Its child spans were already flushed in an earlier envelope, so the test matched a trace that had only the segment. The test now sends a `sentry-trace` header with a random trace id and collects only that trace. Every runtime continues incoming traces: Cloudflare, Bun, and Deno through `wrapServerRequestHandler`, and Node through the http integration. I made the same change in `hono-4-legacy`, because its test file is identical. **How to review:** The two test files have the same diff. I ran `hono-4` (node, cloudflare) and `hono-4-legacy` (node) locally, and the changed tests passed. I couldn't run the bun and deno variants locally. Unrelated to #26199, which changes the timing assertions in the same files. via [automation `sched_91b21536e9d59370f48bbec448287393`](https://junior-prod.sentry.dev/automations/sched_91b21536e9d59370f48bbec448287393) Β· Run ID `sched_91b21536e9d59370f48bbec448287393:1794214800000`. -- [View Session](https://junior-prod.sentry.dev/conversations/agent-dispatch%3Adispatch_d73966730931510e7e24f5361fe87b30) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent-dispatch%3Adispatch_d73966730931510e7e24f5361fe87b30/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../hono-4-legacy/tests/middleware.test.ts | 26 ++++++++++++------- .../hono-4/tests/middleware.test.ts | 26 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) 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 6af9362b0991..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 = [ @@ -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/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts index 6af9362b0991..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 = [ @@ -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( From 4aeb25bc032d317977cea103f2717650e7eae05e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Nov 2026 08:13:28 +0100 Subject: [PATCH 65/92] test(react): Synchronize lazy-route navigation cancellation (#26210) **Why:** Replace the fixed 50 ms delay with a check that the navigation span is active before simulating page hide. Require the cancellation attributes so the test cannot pass when the span ends through its idle timeout. --------- Co-authored-by: GPT-6 --- .../react-router-7-lazy-routes/src/index.tsx | 5 ++ .../tests/spans.test.ts | 59 ++++--------------- 2 files changed, 16 insertions(+), 48 deletions(-) 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 }) => { From 0e9841ec753c9494fb01fa601c1f87f825fcf6a0 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Nov 2026 08:15:01 +0100 Subject: [PATCH 66/92] fix(core): Preserve parentheses in anonymous Node stack paths (#26216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Node stack traces use `at functionName (filename:line:column)` for named frames and `at filename:line:column` when no function name is available. The parser currently treats ` (` inside either format as the start of a named function’s location. This corrupts anonymous frames with paths such as `C:\Program Files (x86)\app.js`, splitting the path into a bogus function name and an incomplete filename. This change only recognizes a function-name prefix when the frame ends with `)`, as the named format requires. Anonymous frames retain their full path and correct `in_app` classification. Closes #26202 Co-authored-by: Codex --- packages/core/src/utils/node-stack-trace.ts | 2 +- .../core/test/lib/utils/stacktrace.test.ts | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) 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/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)'; From 726e947ebb4ad31b77303d3132c8f6ba15c94366 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 10 Nov 2026 07:26:22 +0000 Subject: [PATCH 67/92] test(bundler-plugins): Add integration tests for `sourcemaps.assets` and `sourcemaps.ignore` (#25916) The bundler plugin integration tests had no fixture that set `sourcemaps.assets` or `sourcemaps.ignore`, and the unit tests only check that the globs reach the glob call. Nothing asserted which chunks end up uploaded, which let a gap in the staged upload work go unnoticed. Each of the Rollup, Rolldown, Vite, webpack and esbuild fixtures gets a two-entry build with source maps on. One test ignores `entry2.js`, the other restricts `assets` to `entry1.js`. The `sentry` CLI stub can now record the `sources` of every map in the upload directory, opt-in through `SENTRY_TEST_RECORD_UPLOADED_SOURCES`, so the snapshots show which chunks were uploaded and that every map stays in the output. [These stacked PRs](https://github.com/getsentry/sentry-javascript/pull/25645) will need their own tests to cover the staged upload route. --- .../configs/sourcemaps-assets.config.d.ts | 2 + .../configs/sourcemaps-assets.config.js | 10 +++++ .../configs/sourcemaps-ignore.config.d.ts | 2 + .../configs/sourcemaps-ignore.config.js | 10 +++++ .../esbuild/sourcemaps-assets.config.js | 15 ++++++++ .../esbuild/sourcemaps-assets.test.ts | 36 ++++++++++++++++++ .../esbuild/sourcemaps-ignore.config.js | 15 ++++++++ .../esbuild/sourcemaps-ignore.test.ts | 36 ++++++++++++++++++ .../rolldown/sourcemaps-assets.config.ts | 13 +++++++ .../rolldown/sourcemaps-assets.test.ts | 34 +++++++++++++++++ .../rolldown/sourcemaps-ignore.config.ts | 13 +++++++ .../rolldown/sourcemaps-ignore.test.ts | 37 +++++++++++++++++++ .../rollup4/sourcemaps-assets.config.js | 13 +++++++ .../rollup4/sourcemaps-assets.test.ts | 34 +++++++++++++++++ .../rollup4/sourcemaps-ignore.config.js | 13 +++++++ .../rollup4/sourcemaps-ignore.test.ts | 37 +++++++++++++++++++ .../vite8/sourcemaps-assets.config.ts | 19 ++++++++++ .../fixtures/vite8/sourcemaps-assets.test.ts | 34 +++++++++++++++++ .../vite8/sourcemaps-ignore.config.ts | 19 ++++++++++ .../fixtures/vite8/sourcemaps-ignore.test.ts | 37 +++++++++++++++++++ .../webpack5/sourcemaps-assets.config.js | 21 +++++++++++ .../webpack5/sourcemaps-assets.test.ts | 33 +++++++++++++++++ .../webpack5/sourcemaps-ignore.config.js | 21 +++++++++++ .../webpack5/sourcemaps-ignore.test.ts | 33 +++++++++++++++++ 24 files changed, 537 insertions(+) create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.d.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-assets.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.d.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-ignore.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-assets.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/sourcemaps-ignore.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-assets.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/sourcemaps-ignore.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-assets.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/sourcemaps-ignore.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-assets.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.config.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-ignore.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-assets.test.ts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.config.js create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/sourcemaps-ignore.test.ts 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.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/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.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/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.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/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.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/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.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"], + " + `); +}); From 7bc0a5dd2a4c29eb2be19809f703a9343e8af6d8 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:16:35 +0000 Subject: [PATCH 68/92] ref(server-utils): Add shared isBun, isDeno and isCloudflareWorkers helpers (#26111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Bun/Deno/Cloudflare detection was hand-rolled in several places with slightly different checks (`typeof Bun`, `process.versions.bun`, inline `userAgent === 'Cloudflare-Workers'`). This adds `isBun()`, `isDeno()` and `isCloudflareWorkers()` to `@sentry/server-utils` (exported from the shared `exports.ts`, so they are also available from `/no-diagnostic-channels`) and uses them in server-utils, bun and astro. They are functions rather than module-level consts so unused ones tree-shake cleanly and the check runs when called, not at import. Hono's runtime is now resolved per call instead of at module load. **How to review:** Start with `server-utils/src/utils/runtime.ts`; the rest are mechanical call-site swaps. **Unsure about:** Intentionally left alone: - `remix` (`isCloudflareEnv`): importing from server-utils, even `/no-diagnostic-channels`, pulls `node:async_hooks` into the Hydrogen build and broke the `remix-hydrogen` E2E, so remix keeps its local check - `elysia` (no server-utils dependency, didn't want to add one) - `nextjs` `responseEnd.ts` (nextjs doesn't import the server-utils barrel at runtime today) - `server-runtime-injection/register.ts` (runs at `--import` time and only pulls the `orchestrion/config` subpath, importing the main barrel there seemed risky) - `bun/src/sdk.ts` (`typeof Bun` is needed to narrow for `Bun.version`) Build-time/static detection (export conditions or a `define` flag for Cloudflare) is a possible follow-up, not part of this PR. Bun's own test suite wasn't run locally (no Bun binary in my environment); the change there is `process.versions.bun` β†’ `isBun()`. -- [View Session](https://junior-prod.sentry.dev/conversations/slack%3AD0CA8FXFXAS%3A1793807915.696209) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AD0CA8FXFXAS%3A1793807915.696209/?project=4510944073809921) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy --- packages/astro/src/server/sdk.ts | 4 +-- .../bun/src/integrations/bunHttpServer.ts | 3 +- packages/server-utils/src/ai/flue/index.ts | 12 ++----- packages/server-utils/src/exports.ts | 1 + .../src/integrations/hono/honoIntegration.ts | 14 ++++---- .../server-utils/src/orchestrion/detect.ts | 3 +- .../src/orchestrion/instrumentation.ts | 9 ++---- .../src/utils/diagnosticsChannel.ts | 3 +- packages/server-utils/src/utils/runtime.ts | 18 +++++++++++ .../server-utils/test/utils/runtime.test.ts | 32 +++++++++++++++++++ 10 files changed, 71 insertions(+), 28 deletions(-) create mode 100644 packages/server-utils/src/utils/runtime.ts create mode 100644 packages/server-utils/test/utils/runtime.test.ts diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts index 98fc2d44e198..a6ed97979daa 100644 --- a/packages/astro/src/server/sdk.ts +++ b/packages/astro/src/server/sdk.ts @@ -3,10 +3,10 @@ import { HTTP_SERVER } from '@sentry/conventions/op'; 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; /** @@ -49,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/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/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/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/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index d5ef285cdf62..d8a1acc658cd 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -10,6 +10,7 @@ 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'; @@ -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,7 +43,7 @@ export function honoMiddleware(app: Hono, options: HonoIntegra applyPatches(app); return createHonoRequestMiddleware({ - runtime, + runtime: getHonoRuntime(), shouldHandleError: options.shouldHandleError, }); } @@ -138,7 +136,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/orchestrion/detect.ts b/packages/server-utils/src/orchestrion/detect.ts index c0674371e50a..f388648e4c16 100644 --- a/packages/server-utils/src/orchestrion/detect.ts +++ b/packages/server-utils/src/orchestrion/detect.ts @@ -1,4 +1,5 @@ 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 @@ -36,7 +37,7 @@ export function detectOrchestrionSetup(): void { const { runtime, bundler, channelIntegrations } = marker; if (!runtime && !bundler) { - if (channelIntegrations && typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined') { + if (channelIntegrations && isBun()) { if (!marker.warnedBunRunNotInjected) { marker.warnedBunRunNotInjected = true; consoleSandbox(() => { diff --git a/packages/server-utils/src/orchestrion/instrumentation.ts b/packages/server-utils/src/orchestrion/instrumentation.ts index bf793330c9eb..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(); @@ -104,7 +101,7 @@ 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/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); + }); +}); From b981d4e78436e74986a6301f664dabf4da721935 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:35:03 +0100 Subject: [PATCH 69/92] chore(deps): Remove unused dependencies from workspace packages (#26230) **Why:** Nothing imports or runs these dependencies, and no config or script names them. No other dependency needs them as a peer. Removing them only takes a package.json edit and a lockfile update. They were found with depcheck and a second check that also looked at CLI names in scripts, config and CSS files, and required peers up to two levels deep. Each remaining candidate was then checked by hand. - `bundler-plugins`: `supports-color` and `premove`. `supports-color` is a runtime dependency. Nothing in the package uses it, and the `sentry` CLI only contains a copy bundled inside chalk. - `react`: `history-5` and `@types/history-5` (the tests use `history-4`), and `@types/node-fetch`. - `gatsby`: `@testing-library/react`. `remix`: `@types/express`. `replay-internal`: `@babel/core`. `solidstart`: `node-mock-http`. `nitro`: `unstorage`. `hono`: `wrangler`. `ember`: `@embroider/compat`. - `bun-integration-tests`: `@sentry-internal/test-utils`. - `cloudflare-integration-tests`: `langchain`. Only `@langchain/typesafe/middleware` imports it, and the suite imports the package root. **How to review:** Most of the diff is the lockfile. **Unsure about:** Whether `supports-color` in `@sentry/bundler-plugins` was meant to do anything, such as enable colored output from a dependency. I couldn't find any code that uses it. **Checks run:** I built the affected packages with `build:dev:filter`. These passed: - `test` and `lint:types` for bundler-plugins, react, gatsby, remix, replay-internal, solidstart, nitro, hono and ember - the langchain cloudflare suite and cloudflare `lint:types` - oxfmt and `pnpm dedupe --check` I ran the bundler-plugins and nitro tests without the sandbox's `SENTRY_AUTH_TOKEN`; with it set, a few auth-detection tests fail locally. The bun integration tests were not run because bun isn't installed. Related: the same cleanup for the E2E test apps is in its own PR. via **Francesco Gringl-Novy**. -- [View Session](https://junior-prod.sentry.dev/conversations/slack%3AD0CA8FXFXAS%3A1794301479.516639) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AD0CA8FXFXAS%3A1794301479.516639/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy --- .../bun-integration-tests/package.json | 1 - .../cloudflare-integration-tests/package.json | 1 - packages/bundler-plugins/package.json | 4 +- packages/ember/package.json | 1 - packages/gatsby/package.json | 1 - packages/hono/package.json | 3 +- packages/nitro/package.json | 3 +- packages/react/package.json | 3 - packages/remix/package.json | 1 - packages/replay-internal/package.json | 1 - packages/solidstart/package.json | 1 - pnpm-lock.yaml | 728 +----------------- 12 files changed, 7 insertions(+), 741 deletions(-) diff --git a/dev-packages/bun-integration-tests/package.json b/dev-packages/bun-integration-tests/package.json index 0535ff2a987c..3c7f267680d4 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", diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index b85215f9fd08..26c973b3ba34 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -33,7 +33,6 @@ "@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", diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index 8c4d660899a9..d06afa82e67c 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -120,8 +120,7 @@ "glob": "^14.0.3", "magic-string": "~0.30.8 || ^1.0.0", "oxc-parser": "^0.152.0", - "sentry": "^0.49.0", - "supports-color": "^8.1.1" + "sentry": "^0.49.0" }, "peerDependencies": { "rollup": ">=3.2.0", @@ -143,7 +142,6 @@ "@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", "vite": "^8.3.1", "vitest": "^4.1.11", diff --git a/packages/ember/package.json b/packages/ember/package.json index ca68ba70f8af..aa56b6d39c6f 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -56,7 +56,6 @@ "@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", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 0b131facda3b..707aa6d5e701 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" diff --git a/packages/hono/package.json b/packages/hono/package.json index 2fbb3948dbc9..65189da29486 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -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", diff --git a/packages/nitro/package.json b/packages/nitro/package.json index 817182417c09..322b2719688b 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -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", diff --git a/packages/react/package.json b/packages/react/package.json index 209ffa946e17..ff2a159015f8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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", diff --git a/packages/remix/package.json b/packages/remix/package.json index 83340efe196b..09e2971bacd3 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -93,7 +93,6 @@ "@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", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 989521a888d2..0bad28e67110 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -71,7 +71,6 @@ }, "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.31.0", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index d450cb852aa4..7b0dc9b233c0 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -100,7 +100,6 @@ "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ef0d679ae82..58e140c1b683 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -351,9 +351,6 @@ importers: '@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 @@ -534,9 +531,6 @@ 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) @@ -1170,9 +1164,6 @@ importers: sentry: specifier: ^0.49.0 version: 0.49.0 - supports-color: - specifier: ^8.1.1 - version: 8.1.1 devDependencies: '@babel/core': specifier: ^7.18.5 @@ -1195,9 +1186,6 @@ 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 @@ -1372,9 +1360,6 @@ 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) @@ -1478,9 +1463,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 @@ -1570,9 +1552,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: @@ -1712,9 +1691,6 @@ importers: 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: @@ -1901,12 +1877,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 @@ -1919,9 +1889,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 @@ -2059,9 +2026,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 @@ -2106,9 +2070,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 @@ -2312,9 +2273,6 @@ importers: 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 solid-js: specifier: ^1.9.18 version: 1.9.18 @@ -4286,11 +4244,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==} @@ -4508,12 +4461,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.*'} @@ -8711,9 +8658,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==} @@ -8858,9 +8802,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==} @@ -9689,10 +9630,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'} @@ -9701,18 +9638,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'} @@ -9963,10 +9888,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.*'} @@ -9997,12 +9918,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.*} @@ -10011,10 +9926,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.*'} @@ -10066,18 +9977,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==} @@ -10142,9 +10046,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==} @@ -10210,10 +10111,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==} @@ -10258,10 +10155,6 @@ 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.*'} - browserslist-to-esbuild@2.1.1: resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} engines: {node: '>=18'} @@ -10274,9 +10167,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'} @@ -10431,10 +10321,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==} @@ -10521,10 +10407,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'} @@ -10541,9 +10423,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'} @@ -10731,10 +10610,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==} @@ -10748,10 +10623,6 @@ 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'} @@ -11207,10 +11078,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'} @@ -12102,13 +11969,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'} @@ -12121,10 +11981,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'} @@ -12255,9 +12111,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'} @@ -12280,10 +12133,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'} @@ -12308,10 +12157,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'} @@ -12327,9 +12172,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'} @@ -12353,10 +12195,6 @@ 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} @@ -12552,10 +12390,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'} @@ -12636,14 +12470,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'} @@ -12850,10 +12676,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'} @@ -12994,10 +12816,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'} @@ -13121,10 +12939,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'} @@ -13239,10 +13053,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'} @@ -13567,15 +13377,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'} @@ -14013,9 +13814,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==} @@ -14028,10 +13826,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'} @@ -14153,9 +13947,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'} @@ -14412,10 +14203,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'} @@ -14744,9 +14531,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==} @@ -14990,9 +14774,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==} @@ -15215,10 +14996,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'} @@ -15233,10 +15010,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'} @@ -15335,10 +15108,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.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -15548,10 +15317,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==} @@ -16404,9 +16169,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'} @@ -16757,10 +16519,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'} @@ -16780,10 +16538,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==} @@ -16807,10 +16561,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'} @@ -17012,11 +16762,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'} @@ -17543,10 +17288,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'} @@ -17591,14 +17332,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'} @@ -17880,9 +17613,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==} @@ -17943,17 +17673,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'} @@ -18019,10 +17742,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==} @@ -19095,16 +18814,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'} @@ -19258,10 +18970,6 @@ 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'} @@ -21613,11 +21321,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) @@ -23176,11 +22879,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': {} @@ -23529,59 +23227,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) @@ -27789,8 +27434,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 @@ -27959,11 +27602,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 @@ -29015,20 +28653,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: {} @@ -29397,8 +29027,6 @@ 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)): @@ -29427,12 +29055,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 @@ -29441,11 +29063,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 @@ -29567,15 +29184,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: {} @@ -29620,8 +29233,6 @@ snapshots: binaryextensions@2.3.0: {} - bind-decorator@1.0.11: {} - bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -29748,22 +29359,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 @@ -29855,29 +29450,6 @@ snapshots: dependencies: broccoli-node-api: 1.7.0 - broccoli@4.0.0(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) - transitivePeerDependencies: - - supports-color - browserslist-to-esbuild@2.1.1(browserslist@4.29.0): dependencies: browserslist: 4.29.0 @@ -29891,10 +29463,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: @@ -30112,10 +29680,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: {} @@ -30193,10 +29757,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 @@ -30209,8 +29769,6 @@ snapshots: cli-spinners@2.9.2: {} - cli-width@2.2.1: {} - cli-width@3.0.0: {} client-only@0.0.1: {} @@ -30328,7 +29886,8 @@ snapshots: commander@12.1.0: {} - commander@14.0.3: {} + commander@14.0.3: + optional: true commander@2.20.3: {} @@ -30383,15 +29942,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: {} @@ -30402,14 +29952,6 @@ 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 @@ -30883,8 +30425,6 @@ snapshots: destroy@1.2.0: {} - detect-file@1.0.0: {} - detect-indent@6.0.0: {} detect-libc@2.1.2: {} @@ -31932,20 +31472,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 @@ -31972,10 +31498,6 @@ 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): @@ -32206,10 +31728,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 @@ -32225,10 +31743,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 @@ -32266,18 +31780,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) @@ -32312,8 +31814,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 @@ -32341,13 +31841,6 @@ 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 @@ -32587,10 +32080,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: {} @@ -32695,20 +32184,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 @@ -33036,10 +32511,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: {} @@ -33254,8 +32725,6 @@ snapshots: httpxy@0.5.1: {} - human-signals@1.1.1: {} - human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -33361,22 +32830,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 @@ -33522,8 +32975,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: @@ -33872,33 +33323,6 @@ snapshots: - 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 - xml-name-validator: 5.0.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - jsesc@2.5.2: {} jsesc@3.0.2: {} @@ -34094,6 +33518,7 @@ 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): dependencies: @@ -34343,8 +33768,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.omit@4.18.0: {} - lodash.once@4.1.1: {} lodash.sortby@4.7.0: {} @@ -34353,10 +33776,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 @@ -34512,10 +33931,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 @@ -34925,8 +34340,6 @@ snapshots: mime@4.1.0: {} - mimic-fn@1.2.0: {} - mimic-fn@2.1.0: {} mimic-fn@3.1.0: {} @@ -35258,8 +34671,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): @@ -35907,8 +35318,6 @@ snapshots: - bluebird - supports-color - node-int64@0.4.0: {} - node-mock-http@1.0.4: {} node-releases@2.0.59: {} @@ -36363,10 +35772,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 @@ -36381,10 +35786,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 @@ -36465,15 +35866,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.4.1: dependencies: bl: 4.1.0 @@ -36806,8 +36198,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: @@ -37828,11 +37218,6 @@ 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: {} @@ -37950,7 +37335,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 @@ -38297,11 +37682,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: {} @@ -38320,11 +37700,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: {} @@ -38352,11 +37727,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 @@ -38606,18 +37976,6 @@ 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 @@ -39307,11 +38665,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 @@ -39379,14 +38732,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 @@ -39674,11 +39019,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: {} @@ -39725,14 +39065,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 @@ -39800,16 +39134,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: {} @@ -40359,16 +39683,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) @@ -40992,22 +40306,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 @@ -41301,10 +40603,6 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 - which@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -41380,24 +40678,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 From 41d529a504e51e62b03db3a4518f4cd2e0ba7839 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 10 Nov 2026 11:35:06 +0100 Subject: [PATCH 70/92] fix(core): Use the final segment span name for streamed spans that end before a rename (#26178) **Why:** With span streaming, each span copies the segment span's name into `sentry.segment.name` when it ends. If the segment span is renamed later (Next.js does this on every request once the route is known), spans that ended earlier keep the old name and don't show up when searching by the final name. The span buffer now refreshes the value when the trace is sent, unless the user or `beforeSendSpan` changed it. Renames that Next.js applies only to the serialized segment span in `processSegmentSpan` (late route fill-in, `/_error`, query stripping) are not picked up yet. I'd handle those in a follow-up. Closes #26080 --------- Co-authored-by: Claude Opus 5.5 --- .../tracing/segment-name-update/scenario.ts | 16 +++ .../tracing/segment-name-update/test.ts | 27 ++++ packages/core/src/tracing/sentrySpan.ts | 2 +- .../core/src/tracing/spans/captureSpan.ts | 4 +- packages/core/src/tracing/spans/spanBuffer.ts | 39 +++++- .../lib/tracing/spans/captureSpan.test.ts | 4 + .../test/lib/tracing/spans/spanBuffer.test.ts | 131 +++++++++++++++++- 7 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/segment-name-update/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/segment-name-update/test.ts 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/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/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'); + }); + }); }); From 1d354fd3223d846981b2742249222480bdedc50e Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:36:13 +0100 Subject: [PATCH 71/92] chore(deps): Bump `@sentry/conventions` to 0.32.0 (#26222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** 0.32.0 adds no new constants and doesn't rename or remove any. It mostly fixes doc comments. Its one API change deprecates the `GEN_AI_RERANK` op ("Product does not support rerank spans"). The Vercel AI channel subscriber still maps `rerank` operations to that op. I kept it and suppressed `no-deprecated` on that one line, so rerank spans keep emitting `gen_ai.rerank`. Dropping or remapping them is a product call, not part of a version bump. While sweeping for leftover stand-ins, I also replaced a stale `'gen_ai.extract'` literal and its TODO in the `cloudflare-workers-send-to-sentry` e2e test with `GEN_AI_EXTRACT`, which has been published for a while. **How to review:** Each version bump keeps its file's caret or exact pin. The `nextjs-16-cacheComponents` and `nextjs-16-streaming-cacheComponents` e2e apps, which were still on 0.27.0, now also pin 0.32.0, so every package is on the same version. `minimumReleaseAgeExclude` moves to 0.32.0, and the lockfile has no 0.31.0 entry left. **Unsure about:** Should rerank spans keep `gen_ai.rerank` long term, or should they be dropped or mapped to another op? via [automation `sched_ae2548aa50f44ee9e080350e80e0d775`](https://junior-prod.sentry.dev/automations/sched_ae2548aa50f44ee9e080350e80e0d775) Β· Run ID `sched_ae2548aa50f44ee9e080350e80e0d775:1794297600000`. -- [View Session](https://junior-prod.sentry.dev/conversations/agent-dispatch%3Adispatch_ad30c48b6607dc16f2a564552cae3c36) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent-dispatch%3Adispatch_ad30c48b6607dc16f2a564552cae3c36/?project=4510944073809921) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> --- .../browser-integration-tests/package.json | 2 +- .../cloudflare-integration-tests/package.json | 2 +- .../deno-integration-tests/package.json | 2 +- .../test-applications/angular-19/package.json | 2 +- .../cloudflare-pi-durable/package.json | 2 +- .../package.json | 2 +- .../tests/send-to-sentry.test.ts | 5 +- .../gen-ai-libraries/package.json | 2 +- .../nextjs-16-adapter/package.json | 2 +- .../nextjs-16-cacheComponents/package.json | 2 +- .../package.json | 2 +- .../nextjs-16-tunnel/package.json | 2 +- .../test-applications/nitro-3/package.json | 2 +- .../test-applications/node-eve/package.json | 2 +- .../test-applications/node-hapi/package.json | 2 +- .../node-mastra/package.json | 2 +- .../node-openai-decisions/package.json | 2 +- .../node-pi-durable/package.json | 2 +- .../nuxt-4-static/package.json | 2 +- .../solidstart-static/package.json | 2 +- .../node-integration-tests/package.json | 2 +- packages/angular/package.json | 2 +- packages/astro/package.json | 2 +- packages/aws-serverless/package.json | 2 +- packages/browser-utils/package.json | 2 +- packages/browser/package.json | 2 +- packages/bun/package.json | 2 +- packages/cloudflare/package.json | 2 +- packages/core/package.json | 2 +- packages/deno/package.json | 2 +- packages/effect/package.json | 2 +- packages/elysia/package.json | 2 +- packages/ember/package.json | 2 +- packages/google-cloud-serverless/package.json | 2 +- packages/hono/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nitro/package.json | 2 +- packages/node/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/opentelemetry/package.json | 2 +- packages/profiling-node/package.json | 2 +- packages/react-router/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/replay-internal/package.json | 2 +- packages/server-utils/package.json | 2 +- .../vercel-ai/vercel-ai-dc-subscriber.ts | 3 + packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/vue/package.json | 2 +- pnpm-lock.yaml | 150 +++++++++--------- pnpm-workspace.yaml | 2 +- 56 files changed, 133 insertions(+), 131 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index f00f5d47d843..53a7f806d234 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -61,7 +61,7 @@ "@playwright/test": "1.64.1", "@sentry/browser": "11.11.0", "@sentry/browser-utils": "11.11.0", - "@sentry/conventions": "0.31.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "11.11.0", "@sentry/replay": "11.11.0", "@sentry/replay-canvas": "11.11.0", diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 26c973b3ba34..06c3aa8fce30 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -28,7 +28,7 @@ "@prisma/client": "6.15.0", "@sentry-internal/test-utils": "11.11.0", "@sentry/cloudflare": "11.11.0", - "@sentry/conventions": "0.31.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", "ai": "^6.0.0", diff --git a/dev-packages/deno-integration-tests/package.json b/dev-packages/deno-integration-tests/package.json index b2597575f3c1..ddb21c4507e2 100644 --- a/dev-packages/deno-integration-tests/package.json +++ b/dev-packages/deno-integration-tests/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@sentry-internal/node-integration-tests": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/deno": "11.11.0", "mysql": "^2.18.1", 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 fb5c68db2e3c..8ac36f257112 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -36,7 +36,7 @@ "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", "@playwright/test": "1.64.1", - "@sentry/conventions": "0.31.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", 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 455b99384a8f..fdf0179b54ec 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 @@ -22,7 +22,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.31.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-workers-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/package.json index 9c4d7b9ea3c4..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.31.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/gen-ai-libraries/package.json b/dev-packages/e2e-tests/test-applications/gen-ai-libraries/package.json index cfa72e11537f..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.31.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/nextjs-16-adapter/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-adapter/package.json index 03ef2f7c040e..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.31.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..8b2a76394ee1 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 @@ -38,7 +38,7 @@ "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-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json index c6ee4a2dca46..4b2aaf7d3fc7 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 @@ -35,7 +35,7 @@ "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-tunnel/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/package.json index c079ce13dadd..b5e71ff001b1 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 @@ -41,7 +41,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.31.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/nitro-3/package.json b/dev-packages/e2e-tests/test-applications/nitro-3/package.json index 7b9cdcc38a89..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.31.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 4779ef476d7c..2f9ca8ce29c5 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/package.json +++ b/dev-packages/e2e-tests/test-applications/node-eve/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.31.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "24.x", "typescript": "~5.9.0" 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 ee464da88bd0..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.31.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 f31a2ab9a7d0..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.31.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 62089c547f8e..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.31.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 12fa14dcc544..d1ca67e50551 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,7 +20,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", - "@sentry/conventions": "0.31.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "24.x" }, 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 2e0d9daf20ba..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.31.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/solidstart-static/package.json b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json index 3c222da22783..27e6ec8edf14 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "1.64.1", - "@sentry/conventions": "0.31.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", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index edb41d914cc7..47105987c50a 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -61,7 +61,7 @@ "@sentry-internal/rollup-utils": "11.11.0", "@sentry-internal/test-utils": "11.11.0", "@sentry/aws-serverless": "11.11.0", - "@sentry/conventions": "0.31.0", + "@sentry/conventions": "0.32.0", "@sentry/core": "11.11.0", "@sentry/hono": "11.11.0", "@sentry/node": "11.11.0", diff --git a/packages/angular/package.json b/packages/angular/package.json index 56e66857d5f9..debd43171053 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.31.0", + "@sentry/conventions": "^0.32.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 61615e11416c..1842eb599f2c 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 29fa356d6fea..61fb750a8fec 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -66,7 +66,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index e1a654eea647..20eeec3ef11c 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.31.0", + "@sentry/conventions": "^0.32.0", "web-vitals": "^6.2.3" }, "devDependencies": { diff --git a/packages/browser/package.json b/packages/browser/package.json index 995f4daddab0..267d484a3a42 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/feedback": "11.11.0", "@sentry/replay": "11.11.0", "@sentry/replay-canvas": "11.11.0", diff --git a/packages/bun/package.json b/packages/bun/package.json index dfedfbbb6727..825b9bc34cc3 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/node": "11.11.0", "@sentry/server-utils": "11.11.0" }, diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 0800f7843b63..ccb8599c9d0a 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/opentelemetry": "11.11.0", "@sentry/server-utils": "11.11.0", diff --git a/packages/core/package.json b/packages/core/package.json index f0a2b16e9714..aa8ad4ee798b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -92,6 +92,6 @@ "zod": "^4.6.7" }, "dependencies": { - "@sentry/conventions": "^0.31.0" + "@sentry/conventions": "^0.32.0" } } diff --git a/packages/deno/package.json b/packages/deno/package.json index 9dcd96de03e4..148de4354e80 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -28,7 +28,7 @@ ], "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0" diff --git a/packages/effect/package.json b/packages/effect/package.json index 43c22246785f..c6b742b3d7e5 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0" }, diff --git a/packages/elysia/package.json b/packages/elysia/package.json index 1be217b97693..65c2df11d844 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.31.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "elysia": "^1.4.0" diff --git a/packages/ember/package.json b/packages/ember/package.json index aa56b6d39c6f..adc7011b10d5 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -45,7 +45,7 @@ "@embroider/addon-shim": "^1.11.0", "@embroider/macros": "^1.20.2", "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "decorator-transforms": "^2.3.2" }, diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 9db4358d1aef..f41fd2a95648 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/hono/package.json b/packages/hono/package.json index 65189da29486..2a889561520b 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/server-utils": "11.11.0" }, "peerDependencies": { diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 60b2d8e0224b..76edfe2eedc4 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -54,7 +54,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-utils": "11.11.0" diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index c078bb3b5aa4..1c222d68dfbc 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/opentelemetry": "11.11.0", diff --git a/packages/nitro/package.json b/packages/nitro/package.json index 322b2719688b..c72ee4a0c490 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/node/package.json b/packages/node/package.json index 5f299a4ea431..9264093ed787 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -78,7 +78,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/opentelemetry": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 798f15998c02..33b7dd22cd61 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/bundler-plugins": "11.11.0", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index c7ad5404b17f..9378ec0b381b 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0" }, "peerDependencies": { diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 1e472549e13a..eaa930204ea4 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -50,7 +50,7 @@ "test:watch": "vitest --watch" }, "dependencies": { - "@sentry/conventions": "^0.31.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 229a552dbf78..2f6307674486 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -57,7 +57,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", diff --git a/packages/react/package.json b/packages/react/package.json index ff2a159015f8..7645bcdc97db 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.31.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "react": "17.x || 18.x || 19.x", diff --git a/packages/remix/package.json b/packages/remix/package.json index 09e2971bacd3..6f34bc1c6a1f 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,7 +78,7 @@ "@remix-run/router": "^1.23.4", "@sentry/browser": "11.11.0", "@sentry/bundler-plugins": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 0bad28e67110..57a5c9c9b71a 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -73,7 +73,7 @@ "devDependencies": { "@sentry-internal/replay-worker": "11.11.0", "@sentry-internal/rollup-utils": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/rrweb": "2.45.0", "@sentry/rrweb-snapshot": "2.45.0", "fflate": "0.8.3", diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 511b18ab3e29..57e33f5f4c90 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0" }, "devDependencies": { 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 5c12ef8f98c4..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 @@ -72,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, diff --git a/packages/solid/package.json b/packages/solid/package.json index 555469540092..f5e7ad4504f9 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.31.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 7b0dc9b233c0..f64681a341a9 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.31.0", + "@sentry/conventions": "0.32.0", "h3": "^1.15.10" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8d10df92a5c9..3b1bd5a67264 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@sentry/browser": "11.11.0", - "@sentry/conventions": "^0.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "magic-string": "~0.30.0" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index cc07dca47c54..4f458fa6b7b0 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/node": "11.11.0", "@sentry/server-runtime-injection": "11.11.0", "@sentry/server-utils": "11.11.0", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 43c36aa8cfe9..cb1acf2fb1f9 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.31.0", + "@sentry/conventions": "^0.32.0", "@sentry/core": "11.11.0", "@sentry/node": "11.11.0", "@sentry/react": "11.11.0", diff --git a/packages/vue/package.json b/packages/vue/package.json index c88b9e03fa87..b100c6278401 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.31.0" + "@sentry/conventions": "^0.32.0" }, "peerDependencies": { "@tanstack/vue-router": "^1.64.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58e140c1b683..d18ee8780cf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,8 +301,8 @@ importers: specifier: 11.11.0 version: link:../../packages/browser-utils '@sentry/conventions': - specifier: 0.31.0 - version: 0.31.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -517,8 +517,8 @@ importers: specifier: 11.11.0 version: link:../../packages/cloudflare '@sentry/conventions': - specifier: 0.31.0 - version: 0.31.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -553,8 +553,8 @@ importers: specifier: 11.11.0 version: link:../node-integration-tests '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -694,8 +694,8 @@ importers: specifier: 11.11.0 version: link:../../packages/aws-serverless '@sentry/conventions': - specifier: 0.31.0 - version: 0.31.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../../packages/core @@ -955,8 +955,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1016,8 +1016,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1044,8 +1044,8 @@ importers: packages/aws-serverless: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1078,8 +1078,8 @@ importers: specifier: 11.11.0 version: link:../browser-utils '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1106,8 +1106,8 @@ importers: packages/browser-utils: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1125,8 +1125,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1205,8 +1205,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1242,8 +1242,8 @@ importers: packages/core: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 devDependencies: '@sentry-internal/rollup-utils': specifier: 11.11.0 @@ -1258,8 +1258,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1280,8 +1280,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1305,8 +1305,8 @@ importers: specifier: 11.11.0 version: link:../bun '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1333,8 +1333,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1476,8 +1476,8 @@ importers: packages/google-cloud-serverless: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1516,8 +1516,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1566,8 +1566,8 @@ importers: packages/nestjs: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1612,8 +1612,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1664,8 +1664,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1701,8 +1701,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1757,8 +1757,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1818,8 +1818,8 @@ importers: packages/opentelemetry: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1834,8 +1834,8 @@ importers: packages/profiling-node: dependencies: '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1859,8 +1859,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1926,8 +1926,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -1987,8 +1987,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2077,8 +2077,8 @@ importers: specifier: 11.11.0 version: link:../../dev-packages/rollup-utils '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/rrweb': specifier: 2.45.0 version: 2.45.0 @@ -2145,8 +2145,8 @@ importers: specifier: ^1.9.1 version: 1.10.0 '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2179,8 +2179,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2222,8 +2222,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: 0.31.0 - version: 0.31.0 + specifier: 0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2292,8 +2292,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2329,8 +2329,8 @@ importers: specifier: 11.11.0 version: link:../cloudflare '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2393,8 +2393,8 @@ importers: specifier: 11.11.0 version: link:../bundler-plugins '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -2461,8 +2461,8 @@ importers: specifier: 11.11.0 version: link:../browser '@sentry/conventions': - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.32.0 + version: 0.32.0 '@sentry/core': specifier: 11.11.0 version: link:../core @@ -8089,8 +8089,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.31.0': - resolution: {integrity: sha512-TOXB7z08JPVop4eiYLhZhgWitUYUMX6T1SjLCthaa8YLnNyA3+gbT1ccUinf2EWvzFwBuYPShPB7oYSkKeCFIQ==} + '@sentry/conventions@0.32.0': + resolution: {integrity: sha512-WkSQcDhUUsz1SwxR23LpmyPKba+dgxxiH0DWV4VSM9yaAvhhl87ppW2Ts3g4piIKCVflc31aJeWy+biUbYfsMQ==} engines: {node: '>=14'} '@sentry/node-cpu-profiler@2.4.4': @@ -26577,7 +26577,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@sentry/conventions@0.31.0': {} + '@sentry/conventions@0.32.0': {} '@sentry/node-cpu-profiler@2.4.4': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 12b1a927e453..cf20c2d2596b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -121,7 +121,7 @@ allowBuilds: workerd: true minimumReleaseAgeExclude: - '@opentelemetry/api@1.10.0' - - '@sentry/conventions@0.31.0' + - '@sentry/conventions@0.32.0' - '@sentry/rrdom@2.45.0' - '@sentry/rrweb-snapshot@2.45.0' - '@sentry/rrweb-types@2.45.0' From 4c4b2359cea9f092112960a35168e3d0a0403625 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:01:11 +0000 Subject: [PATCH 72/92] fix(aws-serverless): Omit default port from url.full on function.aws spans (#26227) **Why:** `url.full` on HTTP-triggered Lambda invocations always appended `x-forwarded-port`, so every Function URL and API Gateway request read `https://host:443/path`. We now drop 80/443 for their scheme, like browsers do. We also don't append the port when the host header already has one: an ALB on a custom port produced `host:8080:8080`. **How to review:** Small change in `extractFullUrl` plus unit tests. The value of `url.full` changes for existing users, but the attribute itself stays. Refs #25813 via **Francesco Gringl-Novy**. -- [View Session](https://junior-prod.sentry.dev/conversations/slack%3AD0CA8FXFXAS%3A1794297679.791099) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AD0CA8FXFXAS%3A1794297679.791099/?project=4510944073809921) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy --- .../aws-serverless/src/requestSpanOptions.ts | 5 ++- .../test/requestSpanOptions.test.ts | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 packages/aws-serverless/test/requestSpanOptions.test.ts 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/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', + ); + }); +}); From decf9175bf1d31ee29fb7830582e7077a6d403e5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Nov 2026 12:53:53 +0100 Subject: [PATCH 73/92] fix(nextjs,sveltekit): Hide git revision console on Windows (#26223) **Why:** Git release detection can flash a console window on Windows when config is loaded by a detached process, such as Next.js telemetry on shutdown. Hide the subprocess console in Next.js and SvelteKit, matching the existing bundler-plugin behavior. Closes #26220 --------- Co-authored-by: GPT-6 --- .../src/config/withSentryConfig/buildTime.ts | 2 +- packages/nextjs/test/config/buildTime.test.ts | 20 +++++++++++++++++++ packages/sveltekit/src/vite/sourceMaps.ts | 2 +- .../sveltekit/test/vite/sourceMaps.test.ts | 19 ++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 packages/nextjs/test/config/buildTime.test.ts 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/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/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 7afd0ff088d1..4394e27ad4bf 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -365,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/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 5c6134f71ca5..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! From 3050533107a23fac52c990545f2ab9849e4bbc7d Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:59:18 +0100 Subject: [PATCH 74/92] test(e2e): Remove unused dependencies from E2E test apps (#26231) **Why:** Many test apps install packages that nothing imports or runs. Most are leftovers from project templates, such as jest and supertest in Nest apps, karma in Angular apps, ESLint packages in apps with no ESLint config, and vitest helpers with no unit tests. Others are old workarounds. They make installs slower and give Dependabot more to bump. Every removal is a package.json change only. **How to review:** Groups worth a closer look: - `import-in-the-middle` and `require-in-the-middle` in the Next 16 apps and nextjs-otlp. #17922 added them as a Turbopack/pnpm workaround. No SDK package depends on them or on `@opentelemetry/instrumentation` anymore, and neither do these apps. CI is the real check here. - `isbot`: removed only from React Router apps whose own `entry.server` doesn't use it. Apps without an `entry.server` keep it, because the default one imports it. - ESLint: removed from apps that have no ESLint config. The `lint` scripts in create-remix-app-express(-vite-dev) and remix-hydrogen were removed too; without a config they already failed. - nestjs-webpack keeps `ts-loader`, because webpack mode needs it. nestjs-12 also drops `@nestjs/testing@latest` from its `test:build-latest` script. - `@babel/plugin-proposal-decorators` in the Cloudflare agent apps: `agents` already depends on it. **Unsure about:** Ember apps were not changed, because ember-cli loads addons from package.json without any import. Some dependencies are left in for now: `@astrojs/internal-helpers`, nitro-3 `rolldown`, nuxt-5 `db0`, and the `@types/*` packages the build type-checks against. **Checks run:** oxfmt only. I didn't run the E2E apps locally because they need the packed tarballs, so this relies on CI. Related: the same cleanup for the workspace packages is in its own PR. via **Francesco Gringl-Novy**. -- [View Session](https://junior-prod.sentry.dev/conversations/slack%3AD0CA8FXFXAS%3A1794301479.516639) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AD0CA8FXFXAS%3A1794301479.516639/?project=4510944073809921) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy --- .../test-applications/angular-17/package.json | 11 ----------- .../test-applications/angular-18/package.json | 11 ----------- .../test-applications/angular-19/package.json | 11 ----------- .../test-applications/angular-20/package.json | 11 ----------- .../test-applications/angular-21/package.json | 8 -------- .../test-applications/angular-22/package.json | 8 -------- .../cloudflare-agent/package.json | 2 -- .../cloudflare-autoinstrument/package.json | 1 - .../cloudflare-local-workers/package.json | 3 +-- .../cloudflare-mastra/package.json | 3 +-- .../test-applications/cloudflare-mcp/package.json | 3 +-- .../cloudflare-orchestrion-mysql/package.json | 3 +-- .../cloudflare-think/package.json | 3 +-- .../cloudflare-vercelai-v7/package.json | 3 +-- .../create-remix-app-express-vite-dev/package.json | 8 -------- .../create-remix-app-express/package.json | 14 +------------- .../create-remix-app-v2-non-vite/package.json | 2 -- .../create-remix-app-v2-static/package.json | 3 --- .../create-remix-app-v2/package.json | 3 --- .../hydrogen-react-router-7/package.json | 6 +----- .../nestjs-11-static/package.json | 9 --------- .../test-applications/nestjs-11/package.json | 9 --------- .../test-applications/nestjs-12/package.json | 11 +---------- .../test-applications/nestjs-8/package.json | 9 --------- .../nestjs-basic-with-graphql/package.json | 9 --------- .../test-applications/nestjs-basic/package.json | 9 --------- .../nestjs-distributed-tracing/package.json | 9 --------- .../test-applications/nestjs-fastify/package.json | 9 --------- .../test-applications/nestjs-graphql/package.json | 9 --------- .../test-applications/nestjs-webpack/package.json | 1 - .../nestjs-with-submodules-decorator/package.json | 9 --------- .../nestjs-with-submodules/package.json | 9 --------- .../test-applications/nextjs-15-t3/package.json | 5 ----- .../nextjs-16-cacheComponents/package.json | 5 +---- .../nextjs-16-standalone/package.json | 5 +---- .../nextjs-16-static/package.json | 2 -- .../package.json | 5 +---- .../nextjs-16-streaming/package.json | 2 -- .../nextjs-16-trailing-slash-static/package.json | 5 +---- .../nextjs-16-trailing-slash/package.json | 5 +---- .../nextjs-16-tunnel/package.json | 6 +----- .../test-applications/nextjs-16/package.json | 2 -- .../test-applications/nextjs-orpc/package.json | 9 +-------- .../test-applications/nextjs-otlp/package.json | 4 +--- .../test-applications/node-eve/package.json | 1 - .../node-express-esm-loader/package.json | 1 - .../node-express-esm-without-loader/package.json | 1 - .../node-express-mcp-v1/package.json | 3 +-- .../node-express-mcp-v2/package.json | 3 +-- .../test-applications/node-firebase/package.json | 2 -- .../test-applications/node-flue/package.json | 1 - .../test-applications/node-pi-durable/package.json | 1 - .../node-profiling-cjs/package.json | 1 - .../node-profiling-esm/package.json | 1 - .../test-applications/node-rolldown/package.json | 3 +-- .../test-applications/node-rollup/package.json | 3 +-- .../test-applications/node-vite/package.json | 3 +-- .../test-applications/node-webpack/package.json | 3 +-- .../test-applications/react-19/package.json | 2 -- .../test-applications/react-router-5/package.json | 1 - .../react-router-7-cross-usage/package.json | 4 +--- .../package.json | 1 - .../react-router-7-framework-spa/package.json | 5 +---- .../react-router-7-framework-static/package.json | 3 +-- .../react-router-7-framework/package.json | 3 +-- .../react-router-7-lazy-routes/package.json | 4 +--- .../react-router-8-cross-usage/package.json | 4 +--- .../react-router-8-framework/package.json | 1 - .../test-applications/remix-hydrogen/package.json | 8 -------- .../remix-server-timing/package.json | 1 - .../test-applications/remix-v3/package.json | 1 - .../test-applications/solid-static/package.json | 1 - .../solid-tanstack-router/package.json | 2 -- .../e2e-tests/test-applications/solid/package.json | 1 - .../test-applications/solidstart-2/package.json | 6 +----- .../solidstart-dynamic-import/package.json | 6 +----- .../test-applications/solidstart-spa/package.json | 6 +----- .../solidstart-static/package.json | 6 +----- .../solidstart-top-level-import/package.json | 6 +----- .../test-applications/solidstart/package.json | 6 +----- .../test-applications/supabase-nextjs/package.json | 5 +---- .../test-applications/svelte-5-static/package.json | 1 - .../test-applications/svelte-5/package.json | 1 - .../sveltekit-2-kit-tracing/package.json | 1 - .../sveltekit-2-otlp/package.json | 1 - .../sveltekit-2-static/package.json | 1 - .../sveltekit-2-svelte-5/package.json | 1 - .../sveltekit-2.5.0-twp/package.json | 1 - .../test-applications/sveltekit-2/package.json | 1 - .../sveltekit-3-cloudflare-workers/package.json | 1 - .../test-applications/tanstack-router/package.json | 2 -- .../tanstackstart-react-static/package.json | 2 -- .../tanstackstart-react/package.json | 2 -- .../test-applications/vue-3-static/package.json | 1 - .../e2e-tests/test-applications/vue-3/package.json | 1 - 95 files changed, 36 insertions(+), 365 deletions(-) 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 8ac36f257112..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", @@ -38,15 +35,7 @@ "@playwright/test": "1.64.1", "@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/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-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-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/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/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-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index 8b2a76394ee1..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,12 +28,9 @@ "@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", 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 4b2aaf7d3fc7..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,12 +25,9 @@ "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", 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 b5e71ff001b1..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,13 +30,9 @@ "@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", 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/node-eve/package.json b/dev-packages/e2e-tests/test-applications/node-eve/package.json index 2f9ca8ce29c5..978dc8a2dbd8 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/package.json +++ b/dev-packages/e2e-tests/test-applications/node-eve/package.json @@ -33,7 +33,6 @@ "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@sentry/conventions": "0.32.0", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/node": "24.x", "typescript": "~5.9.0" }, 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-pi-durable/package.json b/dev-packages/e2e-tests/test-applications/node-pi-durable/package.json index d1ca67e50551..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 @@ -21,7 +21,6 @@ "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", "@sentry/conventions": "0.32.0", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@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/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-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 27e6ec8edf14..5a73c4c69571 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -23,14 +23,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-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/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/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", From dbd346f03b3bd37cae18425c5d36ff3a144c34f6 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 10 Nov 2026 13:00:53 +0100 Subject: [PATCH 75/92] chore(deps): Bump dependencies to fix in-range security alerts (#26221) Fixes 18 Dependabot alerts (fast-uri, postcss-selector-parser, ip-address, prismjs, @vitest/mocker, qs) with patch and minor bumps that the existing parent ranges already allow. Also bumps ember-cli-babel to 8.3.2 so @sentry/ember no longer pulls in @babel/runtime 7.12.18; that alert stays open because of the Angular 14 copy. --------- Co-authored-by: Claude Opus 5.5 --- .../bun-integration-tests/package.json | 2 +- .../package.json | 2 +- dev-packages/bundler-tests/package.json | 2 +- .../cloudflare-integration-tests/package.json | 2 +- .../deno-integration-tests/package.json | 2 +- .../node-integration-tests/package.json | 2 +- dev-packages/test-utils/package.json | 2 +- package.json | 2 +- packages/bundler-plugins/package.json | 2 +- pnpm-lock.yaml | 452 ++++++++++-------- 10 files changed, 264 insertions(+), 206 deletions(-) diff --git a/dev-packages/bun-integration-tests/package.json b/dev-packages/bun-integration-tests/package.json index 3c7f267680d4..db35da187547 100644 --- a/dev-packages/bun-integration-tests/package.json +++ b/dev-packages/bun-integration-tests/package.json @@ -24,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/bundler-plugin-integration-tests/package.json b/dev-packages/bundler-plugin-integration-tests/package.json index b255ad7b19a8..71199d0ff14a 100644 --- a/dev-packages/bundler-plugin-integration-tests/package.json +++ b/dev-packages/bundler-plugin-integration-tests/package.json @@ -16,6 +16,6 @@ "premove": "^4.0.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 06c3aa8fce30..1f7aa4671b1b 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -36,7 +36,7 @@ "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/deno-integration-tests/package.json b/dev-packages/deno-integration-tests/package.json index ddb21c4507e2..e7a6c681f4f2 100644 --- a/dev-packages/deno-integration-tests/package.json +++ b/dev-packages/deno-integration-tests/package.json @@ -22,6 +22,6 @@ "@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/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 47105987c50a..659ab71d5fa8 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -82,7 +82,7 @@ "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/test-utils/package.json b/dev-packages/test-utils/package.json index d50ce05a313a..8064e435ebe1 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -61,7 +61,7 @@ "@playwright/test": "1.64.1" }, "dependencies": { - "express": "^4.21.2", + "express": "^4.22.3", "ws": "^8.20.1" }, "devDependencies": { diff --git a/package.json b/package.json index 2e35ff61e95b..92fcf96fc6b7 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,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/bundler-plugins/package.json b/packages/bundler-plugins/package.json index d06afa82e67c..79e615edc486 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -144,7 +144,7 @@ "@types/node": "^20.19.0", "rolldown": "^1.0.0", "vite": "^8.3.1", - "vitest": "^4.1.11", + "vitest": "^4.1.12", "webpack": "5.112.2" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d18ee8780cf1..45bdeec68eb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -274,8 +274,8 @@ 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) @@ -379,8 +379,8 @@ 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: @@ -397,8 +397,8 @@ importers: 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: @@ -457,8 +457,8 @@ importers: 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) 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) @@ -541,8 +541,8 @@ importers: 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) 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) @@ -568,8 +568,8 @@ 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: @@ -757,8 +757,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 @@ -927,8 +927,8 @@ 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 @@ -1193,8 +1193,8 @@ importers: specifier: ^8.3.1 version: 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) 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) @@ -1291,7 +1291,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 @@ -3451,8 +3451,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 @@ -3563,8 +3563,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 @@ -4060,9 +4060,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'} @@ -9225,11 +9222,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 @@ -9242,18 +9239,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/runner@4.1.12': + resolution: {integrity: sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==} - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@vitest/snapshot@4.1.12': + resolution: {integrity: sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==} - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + '@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: @@ -9812,6 +9815,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.*} @@ -10140,6 +10146,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.*} @@ -10155,6 +10164,9 @@ packages: resolution: {integrity: sha512-ZbGVQjivWi0k220fEeIUioN6Y68xjMy0xiLAc0LdieHI99gw+tafU8w0CggBDYVNsJMKUr006AZaM7gNEwCxEg==} engines: {node: 8.* || 10.* || >= 12.*} + broccoli-string-replace@0.1.2: + resolution: {integrity: sha512-QHESTrrrPlKuXQNWsvXawSQbV2g34wCZ5oKgd6bntdOuN8VHxbg1BCBHqVY5HxXJhWelimgGxj3vI7ECkyij8g==} + browserslist-to-esbuild@2.1.1: resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==} engines: {node: '>=18'} @@ -10627,8 +10639,8 @@ packages: 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: @@ -11260,6 +11272,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'} @@ -11299,8 +11315,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 @@ -11991,12 +12007,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: @@ -12078,11 +12094,11 @@ 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==} @@ -12966,8 +12982,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: @@ -13295,6 +13311,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'} @@ -15933,12 +15953,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: @@ -16073,8 +16093,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: @@ -16176,10 +16196,6 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} - engines: {node: '>=0.6'} - qs@6.16.2: resolution: {integrity: sha512-VYdnB5S7/0IeXzBacUtwNAaM6d7Y/PgYnNC7bJCI6meqA/OgnsQbZMtVW8Dt2D1PHgbHGtYmhSoEIgvt7xZ2ZA==} engines: {node: '>=0.6'} @@ -16414,9 +16430,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==} @@ -18662,20 +18675,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: @@ -19778,7 +19791,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: @@ -21181,21 +21194,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 @@ -21306,12 +21319,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 @@ -22704,10 +22717,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 @@ -22909,15 +22918,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)': 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.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 '@csstools/postcss-color-function@1.1.1(postcss@8.4.31)': dependencies: @@ -22965,15 +22974,15 @@ snapshots: '@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)': 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.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 '@csstools/postcss-nested-calc@1.0.0(postcss@8.4.31)': dependencies: @@ -23055,9 +23064,9 @@ snapshots: dependencies: postcss: 8.5.31 - '@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: @@ -23191,10 +23200,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': {} @@ -23268,7 +23277,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 @@ -23284,7 +23293,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 @@ -23709,7 +23718,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': {} @@ -24668,8 +24677,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 @@ -28082,7 +28091,7 @@ snapshots: 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 @@ -28094,20 +28103,20 @@ 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.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))': dependencies: - '@vitest/spy': 4.1.11 + '@vitest/spy': 4.1.12 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -28117,19 +28126,23 @@ snapshots: 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: @@ -28137,6 +28150,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) @@ -28629,7 +28648,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 @@ -28926,6 +28945,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) @@ -29411,6 +29442,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) @@ -29450,6 +29499,13 @@ snapshots: dependencies: broccoli-node-api: 1.7.0 + broccoli-string-replace@0.1.2(supports-color@10.0.0): + dependencies: + broccoli-persistent-filter: 1.4.6(supports-color@10.0.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + browserslist-to-esbuild@2.1.1(browserslist@4.29.0): dependencies: browserslist: 4.29.0 @@ -29956,7 +30012,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - content-disposition@1.0.1: {} + content-disposition@2.0.1: {} content-tag@4.2.0: {} @@ -30079,12 +30135,12 @@ 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 css-declaration-sorter@7.2.0(postcss@8.5.31): dependencies: @@ -30093,12 +30149,12 @@ snapshots: 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + 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)): dependencies: @@ -30613,6 +30669,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + editions@1.3.4: {} + editions@2.3.1: dependencies: errlop: 2.2.0 @@ -30643,11 +30701,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) @@ -30656,7 +30714,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 @@ -30666,6 +30724,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 @@ -30676,11 +30735,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) @@ -30689,7 +30748,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 @@ -30699,6 +30758,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 @@ -30759,7 +30819,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) @@ -31500,15 +31560,15 @@ snapshots: 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 @@ -31531,7 +31591,7 @@ snapshots: parseurl: 1.3.3 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) @@ -31544,12 +31604,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.4.0(supports-color@10.0.0) - content-disposition: 1.0.1 - content-type: 1.0.5 + 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) @@ -31627,7 +31687,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 @@ -31636,7 +31696,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 @@ -31674,9 +31734,9 @@ 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: @@ -32897,7 +32957,7 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.7.2: {} + ip-address@10.7.6: {} ipaddr.js@1.9.1: {} @@ -33169,6 +33229,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 @@ -36447,17 +36513,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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-calc@10.1.1(postcss@8.5.31): dependencies: postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.2.0 postcss-value-parser: 4.2.0 postcss-clamp@4.1.0(postcss@8.4.31): @@ -36537,27 +36603,27 @@ snapshots: 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-discard-comments@7.0.8(postcss@8.5.31): dependencies: postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.2.0 postcss-discard-duplicates@7.0.4(postcss@8.5.31): dependencies: @@ -36596,22 +36662,22 @@ snapshots: 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-font-variant@5.0.0(postcss@8.4.31): dependencies: @@ -36702,7 +36768,7 @@ snapshots: caniuse-api: 3.0.0 cssnano-utils: 5.0.3(postcss@8.5.31) postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.2.0 postcss-minify-font-values@7.0.3(postcss@8.5.31): dependencies: @@ -36729,7 +36795,7 @@ snapshots: caniuse-api: 3.0.0 cssesc: 3.0.0 postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.2.0 postcss-modules-extract-imports@3.0.0(postcss@8.4.31): dependencies: @@ -36739,13 +36805,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: @@ -36754,15 +36820,15 @@ 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): 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.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-normalize-charset@7.0.3(postcss@8.5.31): dependencies: @@ -36960,12 +37026,12 @@ snapshots: 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + postcss-selector-parser: 6.1.6 postcss-reduce-initial@7.0.9(postcss@8.5.31): dependencies: @@ -36989,21 +37055,19 @@ snapshots: 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): dependencies: postcss: 8.5.31 - postcss-selector-parser: 6.1.4 + 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): @@ -37015,7 +37079,7 @@ snapshots: postcss-unique-selectors@7.0.7(postcss@8.5.31): dependencies: postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + postcss-selector-parser: 7.2.0 postcss-url@10.1.3(postcss@8.5.31): dependencies: @@ -37133,7 +37197,7 @@ snapshots: transitivePeerDependencies: - magicast - prismjs@1.29.0: {} + prismjs@1.30.0: {} private@0.1.8: {} @@ -37222,10 +37286,6 @@ snapshots: pure-rand@6.1.0: {} - qs@6.14.2: - dependencies: - side-channel: 1.1.1 - qs@6.16.2: dependencies: es-define-property: 1.0.1 @@ -37535,8 +37595,6 @@ snapshots: regenerate@1.4.2: {} - regenerator-runtime@0.13.10: {} - regenerator-runtime@0.13.9: {} regex-parser@2.2.11: {} @@ -37647,7 +37705,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: @@ -38459,7 +38517,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: @@ -38783,7 +38841,7 @@ snapshots: dependencies: browserslist: 4.29.0 postcss: 8.5.31 - postcss-selector-parser: 7.1.5 + 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)): dependencies: @@ -40111,15 +40169,15 @@ snapshots: 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) - 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.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.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 @@ -40136,7 +40194,7 @@ snapshots: 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' @@ -40152,15 +40210,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.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.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 @@ -40177,7 +40235,7 @@ snapshots: 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' @@ -40363,7 +40421,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)) From 2f3c8a9448451feee7192fb51e897dfe15155b52 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 10 Nov 2026 13:01:20 +0100 Subject: [PATCH 76/92] test(e2e): Fix cloudflare-pi-durable builds (#26235) ## What Both `cloudflare-pi-durable` e2e variants build again. The app imports `Type` from `typebox` and pins `@earendil-works/chord`, like `node-pi-durable` does. ## Why pi-ai 1.5.0 no longer re-exports `Type`, and chord 1.5.1 removed `copyJson`, which pi-durable 1.2.0 still imports through its `^1.2.0` range. --------- Co-authored-by: Claude Opus 5.5 --- .../test-applications/cloudflare-pi-durable/package.json | 6 ++++-- .../cloudflare-pi-durable/src/assistant.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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 fdf0179b54ec..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,14 +8,16 @@ "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", 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'; From 5a4ac0a1731e9441bd2d9f173236212cdd94a506 Mon Sep 17 00:00:00 2001 From: mq1 <74494945+chan-mai@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:05:13 +0900 Subject: [PATCH 77/92] test(nuxt): Keep Nitro 2 WebSocket handlers resolvable with the managed tunnel (#26082) **Why:** The tunnel plugin for Nitro 2, added in #25418, was adding a layer with a route of `''` to the top of the h3 application stack. Because the h3 WebSocket handler resolution logic does not skip layers with a route of `''`, this layer matched all paths and prevented the `upgrade` hook of user-defined WebSocket handlers in the app from being called. Since this layer was removed in #26001, this PR makes no code changes. Only a test is added to prevent the same issue from recurring. **How to review:** The added tests register the plugin in an app that uses h3's `createApp` and `createRouter` with the router mounted at `/`, just like Nitro. They then verify that Nuxt's `app.baseURL` is passed as the tunnel's `basePath`, and that the resolved WebSocket handlers include the app's `upgrade` hook. **Unsure about:** E2E tests using WebSockets in an actual Nuxt app have not been added. Refs #25991 --- .../plugins/tunnel-legacy.server.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts 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); + }); +}); From e60350c698809568c36d2fb7ad9725b88c52e50c Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:08:13 +0100 Subject: [PATCH 78/92] chore: Add external contributor to CHANGELOG.md (#26238) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #26082 Co-authored-by: s1gr1d <32902192+s1gr1d@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e84c8acbf8..92a81c317c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, and @arthurfiorette. Thank you for your contributions! +Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, and @chan-mai. Thank you for your contributions! ### Important Changes From e76488bad88328d04bb96feac1daae292b1fef2d Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:11:53 +0100 Subject: [PATCH 79/92] fix(hono): Name routes without c.req.matchedRoutes (#25454) On Hono 5, every request got the span name `GET ` and an empty `http.route`. With this PR, names look the same as on Hono 4: `GET /users/:id`, with `http.route` set to `/users/:id`. Why they broke: - Hono 5 removes `c.req.matchedRoutes`. - `compose` resets `c.req.routeIndex` after `next()` ([honojs/hono#4424](https://github.com/honojs/hono/pull/4424)), so the Sentry middleware cannot see which handler responded. How the name is built now: 1. The Sentry middleware reads the match result through the getter that [`hono/route`](https://github.com/honojs/hono/blob/v5.0.0-rc.1/src/helper/route/index.ts#L32-L34) uses (`matchResult.ts`). 2. Before the handlers run, it names the span after the first matched route handler, which is the one Hono runs first. 3. If more than one route handler matched, the SDK wraps them, so the handler that starts last renames the span. If only one matched, the name from step 2 is already correct. The SDK does not use arity (the number of parameters) to decide, because it cannot tell if a handler calls `next()`. Rest parameters and a default `next` hide it. Small changes on Hono 4 too: events captured in a handler get that handler's route (not a later catch-all), and a 404 keeps the runtime name `GET` instead of `GET /*`. Refs #25223 --------- Co-authored-by: Claude Opus 5.5 --- .../hono-4-legacy/package.json | 1 + .../src/route-groups/test-route-patterns.ts | 21 ++ .../tests/route-patterns.test.ts | 27 +- .../test-applications/hono-4/package.json | 1 + .../src/route-groups/test-route-patterns.ts | 21 ++ .../hono-4/tests/route-patterns.test.ts | 27 +- .../suites/hono-sdk/scenario.mjs | 57 ++++- .../suites/hono-sdk/test.ts | 85 +++++++ .../suites/hono/scenario-honoMiddleware.mjs | 38 +++ .../suites/hono/scenario.mjs | 50 ++++ .../suites/hono/test.ts | 180 +++++++++++--- .../utils/runner/createRunner.ts | 4 +- .../test/shared/middlewareHandlers.test.ts | 18 +- .../hono/test/shared/resolveRouteName.test.ts | 234 ++++++++++++++++++ .../src/integrations/hono/constants.ts | 6 + .../integrations/hono/createHonoMiddleware.ts | 6 +- .../src/integrations/hono/honoIntegration.ts | 13 +- .../src/integrations/hono/matchResult.ts | 103 +++++++- .../integrations/hono/middlewareHandlers.ts | 45 +++- .../src/integrations/hono/resolveRouteName.ts | 62 ++--- .../integrations/hono/wrapMiddlewareSpan.ts | 2 +- .../hono/sentryOriginalTraps.test.ts | 22 ++ 22 files changed, 915 insertions(+), 108 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/hono/scenario-honoMiddleware.mjs create mode 100644 packages/hono/test/shared/resolveRouteName.test.ts 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/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/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/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/utils/runner/createRunner.ts b/dev-packages/node-integration-tests/utils/runner/createRunner.ts index db07a127ff12..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; @@ -791,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/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/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 d8a1acc658cd..b97d2b13e307 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -15,7 +15,7 @@ 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'; @@ -48,8 +48,7 @@ export function honoMiddleware(app: Hono, options: HonoIntegra }); } -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]); @@ -61,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. * @@ -80,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 `/*` 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/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/wrapMiddlewareSpan.ts b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts index 10ccdb727ac6..5ba3a80bafd4 100644 --- a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts +++ b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts @@ -9,7 +9,7 @@ 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'; diff --git a/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts b/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts index 183a7820e133..16687da1db8a 100644 --- a/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts +++ b/packages/server-utils/test/integrations/hono/sentryOriginalTraps.test.ts @@ -1,5 +1,7 @@ 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'; @@ -21,4 +23,24 @@ describe('sentryOriginalTraps', () => { 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()); + }); }); From f78206827828a9cc1646a53607380d0c2f7e3c36 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 10 Nov 2026 08:40:47 -0500 Subject: [PATCH 80/92] chore(deps): Bump rrweb to 2.46.0 (#26241) Bumps `@sentry/rrweb` and friends to 2.46.0 so the next SDK release ships the latest rrweb. --- .../browser-integration-tests/package.json | 4 +- packages/replay-canvas/package.json | 2 +- packages/replay-internal/package.json | 4 +- pnpm-lock.yaml | 54 +++++++++---------- pnpm-workspace.yaml | 8 +-- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 53a7f806d234..542cc570aede 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -65,8 +65,8 @@ "@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/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 5c8f8cc28aba..c73027c08762 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -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 57a5c9c9b71a..cb57d5650c9b 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -74,8 +74,8 @@ "@sentry-internal/replay-worker": "11.11.0", "@sentry-internal/rollup-utils": "11.11.0", "@sentry/conventions": "^0.32.0", - "@sentry/rrweb": "2.45.0", - "@sentry/rrweb-snapshot": "2.45.0", + "@sentry/rrweb": "2.46.0", + "@sentry/rrweb-snapshot": "2.46.0", "fflate": "0.8.3", "jsdom-worker": "^0.3.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45bdeec68eb7..ba71c1458013 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,11 +313,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 @@ -2058,8 +2058,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: @@ -2080,11 +2080,11 @@ importers: 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 @@ -8098,17 +8098,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==} @@ -26598,22 +26598,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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cf20c2d2596b..75d66d27027b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -122,8 +122,8 @@ allowBuilds: minimumReleaseAgeExclude: - '@opentelemetry/api@1.10.0' - '@sentry/conventions@0.32.0' - - '@sentry/rrdom@2.45.0' - - '@sentry/rrweb-snapshot@2.45.0' - - '@sentry/rrweb-types@2.45.0' - - '@sentry/rrweb@2.45.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 From b5a927523c1ddbe336a1460553b37df8baefa5bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:45:18 +0100 Subject: [PATCH 81/92] feat(deps): Bump ws from 8.18.0 to 8.22.2 (#26242) Bumps [ws](https://github.com/websockets/ws) from 8.18.0 to 8.22.2.
Release notes

Sourced from ws's releases.

8.22.2

Bug fixes

  • The userinfo of the URL is now percent-decoded (#2352).
  • The WebSocket constructor no longer mutates http: or https: url.URL objects (#2353).

8.22.1

Bug fixes

  • The path of IPC URLs is no longer truncated at : (#2344).

Other notable changes

8.22.0

Features

  • Introduced the protocols option (8b918b01).

Bug fixes

  • Calling websocket.close() with invalid arguments no longer transitions the state to WebSocket.CLOSING (#2337).

8.21.3

Bug fixes

  • The server now correctly rejects permessage-deflate offers if the incoming client_max_window_bits parameter value is smaller than its configured clientMaxWindowBits (e97a20ea).

8.21.2

Bug fixes

  • Fixed a test for CITGM (2eb3be0b).

8.21.1

Bug fixes

... (truncated)

Commits
  • 2582a67 [dist] 8.22.2
  • 3eec252 [perf] Do not retain the read buffer via the close reason (#2354)
  • 9757c69 [fix] Do not modify the url.URL object (#2353)
  • e23d55e [fix] Percent-decode the userinfo of the URL (#2352)
  • 335a610 [perf] Do not retain the read buffer via the frame mask (#2350)
  • f40ccff [doc] Adjust Node.js version requirement in README (#2348)
  • c18a80a [dist] 8.22.1
  • 2ab3d3e [minor] Use http.websocket{M,Unm}ask() if possible (#2347)
  • c9efa88 [ci] Test on node 27
  • aa91518 [test] Skip test on Node.js versions where it does not apply (#2340)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=8.18.0&new-version=8.22.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 168 ++++++++++++++++++++++++------------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba71c1458013..004df226cd37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -491,16 +491,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 @@ -533,7 +533,7 @@ importers: version: 4.13.11 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) @@ -623,13 +623,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) @@ -650,16 +650,16 @@ 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) @@ -827,7 +827,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 @@ -881,7 +881,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 @@ -931,7 +931,7 @@ importers: 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 @@ -19102,8 +19102,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 @@ -23124,9 +23124,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' @@ -23141,7 +23141,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 @@ -23150,7 +23150,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: @@ -23170,10 +23170,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 @@ -23737,10 +23737,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)) @@ -23859,7 +23859,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: @@ -23872,7 +23872,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: @@ -24459,22 +24459,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 @@ -24487,12 +24487,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 @@ -24503,29 +24503,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 @@ -24534,11 +24534,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: @@ -24547,11 +24547,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 @@ -24559,20 +24559,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' @@ -24583,12 +24583,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': {} @@ -24624,7 +24624,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: @@ -24964,7 +24964,7 @@ snapshots: 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)) which: 6.0.1 - ws: 8.22.1 + ws: 8.22.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -33320,7 +33320,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 @@ -33353,7 +33353,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 @@ -33382,7 +33382,7 @@ snapshots: 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 @@ -33569,12 +33569,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' @@ -33586,15 +33586,15 @@ snapshots: - 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 @@ -33605,7 +33605,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: @@ -35893,30 +35893,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: @@ -40436,7 +40436,7 @@ snapshots: 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 + ws: 8.22.2 transitivePeerDependencies: - bufferutil - debug @@ -40801,7 +40801,7 @@ snapshots: ws@8.21.0: {} - ws@8.22.1: {} + ws@8.22.2: {} wsl-utils@0.3.1: dependencies: From 2e4231e31f67d8055560978297090328c2e035c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:00:51 +0100 Subject: [PATCH 82/92] chore(deps-dev): Bump vite from 8.3.5 to 8.3.6 (#26243) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.3.5 to 8.3.6.
Release notes

Sourced from vite's releases.

v8.3.6

Bug Fixes

  • await overlapping plugin container close calls (#23935) (175bbeb)
  • build: resolve dynamic imports with queries appended when collecting CSS dependencies (#23909) (df36558)
  • build: use stable module ids in chunk import map hash signatures (fix #23975) (#23976) (8069835)
  • bundled-dev: allow named imports from Node built-ins (#23858) (4260c75)
  • bundled-dev: reload when hmr cannot run an entry again (#23849) (a236886)
  • deps: update @rollup/plugin-alias to v6.0.1 (#23862) (67c965b)
  • deps: update all non-major dependencies (#23896) (46b91c5)
  • deps: update all non-major dependencies (#23966) (b3cb057)
  • deps: update rolldown-related dependencies (#23868) (a63eb87)
  • deps: update rolldown-related dependencies (#23967) (0105733)
  • dev: escape ANSI escape sequence for server.forwardConsole (#23933) (4f340fd)
  • html: remove adjacent module tags on the first line (#23823) (eab1dc2)
  • ignore pnpm's .modules.yaml as a Vite Task input (#23634) (95c2daf)
  • optimizer: detect cache dir precisely (#23912) (cc9bc21)
  • optimizer: resolve subpath imports (#) with Vite's resolver (#23959) (0fdd81e)
  • respect build.sourcemap from environment (#23879) (82e7aa0)
  • server: escape HTML attributes (#23995) (a270391)
  • server: preserve $ patterns in static alias replacements (#23703) (1b6ed05)
  • ssr: convert external require against built-in modules to import for ssr.target=webworker (#23759) (a711f1c)
  • use top-level input for SSR build when build.ssr: true (#23898) (90bc034)
  • wasm: reject .wasm outside server.fs (#23914) (64e2467)
  • watch files that have a query or hash referenced with new URL(..., import.meta.url) (#23780) (e954a9c)
  • worker: keep chunks conflicted with unused workers that are still used by a live worker (#23956) (7139ef3)

Performance Improvements

Miscellaneous Chores

Build System

  • generate sourcemaps in import-meta-current-dirname plugin (#23904) (20d355a)
Changelog

Sourced from vite's changelog.

8.3.6 (2026-11-05)

Bug Fixes

  • await overlapping plugin container close calls (#23935) (175bbeb)
  • build: resolve dynamic imports with queries appended when collecting CSS dependencies (#23909) (df36558)
  • build: use stable module ids in chunk import map hash signatures (fix #23975) (#23976) (8069835)
  • bundled-dev: allow named imports from Node built-ins (#23858) (4260c75)
  • bundled-dev: reload when hmr cannot run an entry again (#23849) (a236886)
  • deps: update @rollup/plugin-alias to v6.0.1 (#23862) (67c965b)
  • deps: update all non-major dependencies (#23896) (46b91c5)
  • deps: update all non-major dependencies (#23966) (b3cb057)
  • deps: update rolldown-related dependencies (#23868) (a63eb87)
  • deps: update rolldown-related dependencies (#23967) (0105733)
  • dev: escape ANSI escape sequence for server.forwardConsole (#23933) (4f340fd)
  • html: remove adjacent module tags on the first line (#23823) (eab1dc2)
  • ignore pnpm's .modules.yaml as a Vite Task input (#23634) (95c2daf)
  • optimizer: detect cache dir precisely (#23912) (cc9bc21)
  • optimizer: resolve subpath imports (#) with Vite's resolver (#23959) (0fdd81e)
  • respect build.sourcemap from environment (#23879) (82e7aa0)
  • server: escape HTML attributes (#23995) (a270391)
  • server: preserve $ patterns in static alias replacements (#23703) (1b6ed05)
  • ssr: convert external require against built-in modules to import for ssr.target=webworker (#23759) (a711f1c)
  • use top-level input for SSR build when build.ssr: true (#23898) (90bc034)
  • wasm: reject .wasm outside server.fs (#23914) (64e2467)
  • watch files that have a query or hash referenced with new URL(..., import.meta.url) (#23780) (e954a9c)
  • worker: keep chunks conflicted with unused workers that are still used by a live worker (#23956) (7139ef3)

Performance Improvements

Miscellaneous Chores

Build System

  • generate sourcemaps in import-meta-current-dirname plugin (#23904) (20d355a)
Commits
  • 0fd9a29 release: v8.3.6 (#23996)
  • eab1dc2 fix(html): remove adjacent module tags on the first line (#23823)
  • a270391 fix(server): escape HTML attributes (#23995)
  • 7139ef3 fix(worker): keep chunks conflicted with unused workers that are still used b...
  • 0fdd81e fix(optimizer): resolve subpath imports (#) with Vite's resolver (#23959)
  • 8069835 fix(build): use stable module ids in chunk import map hash signatures (fix #2...
  • 95c2daf fix: ignore pnpm's .modules.yaml as a Vite Task input (#23634)
  • b3cb057 fix(deps): update all non-major dependencies (#23966)
  • 0105733 fix(deps): update rolldown-related dependencies (#23967)
  • 90bc034 fix: use top-level input for SSR build when build.ssr: true (#23898)
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/astro/package.json | 2 +- packages/bundler-plugins/package.json | 2 +- packages/cloudflare/package.json | 2 +- packages/ember/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/react-router/package.json | 2 +- packages/remix/package.json | 2 +- packages/server-utils/package.json | 2 +- packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- pnpm-lock.yaml | 1126 ++++++++++----------- 14 files changed, 576 insertions(+), 576 deletions(-) diff --git a/packages/astro/package.json b/packages/astro/package.json index 1842eb599f2c..fe40f775f0dc 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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", diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index 79e615edc486..0fb3dcc6d30e 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -143,7 +143,7 @@ "@types/babel__core": "^7.20.5", "@types/node": "^20.19.0", "rolldown": "^1.0.0", - "vite": "^8.3.1", + "vite": "^8.3.6", "vitest": "^4.1.12", "webpack": "5.112.2" }, diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index ccb8599c9d0a..b4e3ffbe6e9d 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -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": { diff --git a/packages/ember/package.json b/packages/ember/package.json index adc7011b10d5..657e11d744d0 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -62,7 +62,7 @@ "@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/nuxt/package.json b/packages/nuxt/package.json index 33b7dd22cd61..9dd47e0ccbb8 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -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": { diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 2f6307674486..c495545c25fd 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -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", diff --git a/packages/remix/package.json b/packages/remix/package.json index 6f34bc1c6a1f..2ffe8415176a 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -98,7 +98,7 @@ "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", diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 57e33f5f4c90..f230c704670e 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -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": { diff --git a/packages/solid/package.json b/packages/solid/package.json index f5e7ad4504f9..d57429c2a6bb 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -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": { diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index f64681a341a9..2e068dc16ef0 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -102,7 +102,7 @@ "nitropack": "^2.13.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": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3b1bd5a67264..576fae116d97 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -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", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 4f458fa6b7b0..33d5b695eb5e 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -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", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index cb1acf2fb1f9..acbd60baba9c 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 004df226cd37..9df2789b99bd 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 @@ -278,7 +278,7 @@ importers: 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 @@ -332,7 +332,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,10 +341,10 @@ 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: @@ -455,13 +455,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.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: @@ -479,7 +479,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 @@ -539,7 +539,7 @@ importers: 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.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) @@ -683,7 +683,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 @@ -966,7 +966,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) @@ -1038,8 +1038,8 @@ 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: @@ -1190,14 +1190,14 @@ importers: specifier: ^1.0.0 version: 1.2.16 vite: - specifier: ^8.3.1 - version: 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) + 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.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: @@ -1233,8 +1233,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) @@ -1365,7 +1365,7 @@ importers: 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) @@ -1379,8 +1379,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: @@ -1471,7 +1471,7 @@ 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: @@ -1687,7 +1687,7 @@ 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 @@ -1746,7 +1746,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 @@ -1780,7 +1780,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 @@ -1795,22 +1795,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) @@ -1949,7 +1949,7 @@ importers: 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) @@ -1972,8 +1972,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: @@ -2042,8 +2042,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: @@ -2153,7 +2153,7 @@ importers: 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 @@ -2167,11 +2167,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: @@ -2210,11 +2210,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: @@ -2257,7 +2257,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) @@ -2272,19 +2272,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)) + 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: @@ -2306,7 +2306,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) @@ -2314,8 +2314,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: @@ -2367,10 +2367,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 @@ -2378,8 +2378,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: @@ -2418,8 +2418,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: {} @@ -15992,8 +15992,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: @@ -18616,8 +18616,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: @@ -19377,11 +19377,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) @@ -19394,15 +19394,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) @@ -19410,10 +19410,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 @@ -19421,28 +19421,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) @@ -19469,12 +19469,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 @@ -19610,7 +19610,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 @@ -19619,8 +19619,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: @@ -22837,12 +22837,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: @@ -22922,10 +22922,10 @@ snapshots: postcss: 8.4.31 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.6) - postcss: 8.5.31 + postcss: 8.5.32 postcss-selector-parser: 6.1.6 '@csstools/postcss-color-function@1.1.1(postcss@8.4.31)': @@ -22934,10 +22934,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)': @@ -22945,9 +22945,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)': @@ -22955,9 +22955,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)': @@ -22966,10 +22966,10 @@ 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)': @@ -22978,10 +22978,10 @@ snapshots: postcss: 8.4.31 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.6) - postcss: 8.5.31 + postcss: 8.5.32 postcss-selector-parser: 6.1.6 '@csstools/postcss-nested-calc@1.0.0(postcss@8.4.31)': @@ -22989,9 +22989,9 @@ snapshots: 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)': @@ -22999,9 +22999,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)': @@ -23010,10 +23010,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)': @@ -23021,9 +23021,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)': @@ -23031,9 +23031,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)': @@ -23041,9 +23041,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)': @@ -23051,18 +23051,18 @@ 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.6)': dependencies: @@ -23088,17 +23088,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' @@ -23327,7 +23327,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) @@ -23345,7 +23345,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 @@ -24786,11 +24786,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: @@ -24901,11 +24901,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 @@ -24929,11 +24929,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 @@ -24960,9 +24960,9 @@ 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.2 transitivePeerDependencies: @@ -25025,7 +25025,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 @@ -25043,7 +25043,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 @@ -25058,7 +25058,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 @@ -25076,7 +25076,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 @@ -25091,7 +25091,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 @@ -25109,7 +25109,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 @@ -25124,7 +25124,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) @@ -25138,11 +25138,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 @@ -25198,7 +25198,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) @@ -25212,11 +25212,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 @@ -25289,15 +25289,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.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.31) + 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 @@ -25309,13 +25309,13 @@ 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 @@ -25992,10 +25992,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 @@ -26019,12 +26019,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 @@ -26039,11 +26039,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 @@ -26060,7 +26060,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 @@ -26083,7 +26083,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 @@ -26130,7 +26130,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 @@ -26160,7 +26160,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 @@ -26673,11 +26673,11 @@ snapshots: 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.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' @@ -27091,11 +27091,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 @@ -27107,8 +27107,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 @@ -27163,11 +27163,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 @@ -27179,21 +27179,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 @@ -27201,22 +27201,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 @@ -27224,7 +27224,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) @@ -27233,7 +27233,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 @@ -27270,7 +27270,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) @@ -27279,7 +27279,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: @@ -28049,7 +28049,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 @@ -28060,18 +28060,18 @@ 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.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: @@ -28114,13 +28114,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.12(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.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: @@ -28274,7 +28274,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': @@ -29011,13 +29011,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: @@ -29060,21 +29060,21 @@ snapshots: 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: @@ -30051,15 +30051,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: @@ -30137,26 +30137,26 @@ snapshots: postcss: 8.4.31 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: 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.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: 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 @@ -30166,15 +30166,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: @@ -30217,49 +30217,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: @@ -30503,11 +30503,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: @@ -32620,7 +32620,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 @@ -32628,10 +32628,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 @@ -32639,7 +32639,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: @@ -32843,12 +32843,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' @@ -33618,11 +33618,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: @@ -33680,11 +33680,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: @@ -34416,10 +34416,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: @@ -34475,41 +34475,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 @@ -34859,9 +34859,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 @@ -34882,7 +34882,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) @@ -34903,7 +34903,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' @@ -34935,7 +34935,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) @@ -34956,7 +34956,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' @@ -34988,7 +34988,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) @@ -35053,7 +35053,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 @@ -35097,7 +35097,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.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.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) @@ -35162,7 +35162,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.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.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 @@ -35206,7 +35206,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) @@ -35271,7 +35271,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 @@ -35529,16 +35529,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) @@ -35555,7 +35555,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 @@ -35582,8 +35582,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) @@ -36515,14 +36515,14 @@ snapshots: postcss: 8.4.31 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: 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: 8.5.32 postcss-selector-parser: 7.2.0 postcss-value-parser: 4.2.0 @@ -36531,9 +36531,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): @@ -36541,9 +36541,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): @@ -36551,9 +36551,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): @@ -36561,23 +36561,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): @@ -36585,9 +36585,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): @@ -36595,9 +36595,9 @@ 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): @@ -36605,9 +36605,9 @@ snapshots: postcss: 8.4.31 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: 8.5.32 postcss-selector-parser: 6.1.6 postcss-dir-pseudo-class@6.0.5(postcss@8.4.31): @@ -36615,27 +36615,27 @@ snapshots: postcss: 8.4.31 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: 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: 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: @@ -36643,10 +36643,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): @@ -36654,9 +36654,9 @@ 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): @@ -36664,9 +36664,9 @@ snapshots: postcss: 8.4.31 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: 8.5.32 postcss-selector-parser: 6.1.6 postcss-focus-within@5.0.4(postcss@8.4.31): @@ -36674,35 +36674,35 @@ snapshots: postcss: 8.4.31 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: 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): @@ -36716,9 +36716,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: @@ -36726,75 +36726,75 @@ 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 + 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: 8.5.32 postcss-selector-parser: 7.2.0 postcss-modules-extract-imports@3.0.0(postcss@8.4.31): @@ -36824,69 +36824,69 @@ snapshots: postcss: 8.4.31 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.6) - postcss: 8.5.31 + 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): @@ -36894,27 +36894,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): @@ -36970,57 +36970,57 @@ 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): @@ -37028,38 +37028,38 @@ snapshots: postcss: 8.4.31 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: 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.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: 8.5.32 postcss-selector-parser: 6.1.6 postcss-selector-parser@6.1.6: @@ -37070,32 +37070,32 @@ snapshots: dependencies: 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: 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: @@ -37104,7 +37104,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 @@ -37135,7 +37135,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 @@ -37143,7 +37143,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 @@ -38039,11 +38039,11 @@ snapshots: 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 @@ -38088,33 +38088,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: {} @@ -38553,12 +38553,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: @@ -38837,19 +38837,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: 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: @@ -39020,16 +39020,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 @@ -39395,26 +39395,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: {} @@ -39480,7 +39480,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 @@ -39494,7 +39494,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 @@ -39509,7 +39509,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.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.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 @@ -39523,7 +39523,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.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.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 @@ -39538,7 +39538,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 @@ -39552,7 +39552,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 @@ -39677,7 +39677,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.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.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 @@ -39687,9 +39687,9 @@ snapshots: 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.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) - 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 @@ -39698,8 +39698,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: @@ -39871,7 +39871,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)) @@ -39892,7 +39892,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.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.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 @@ -39958,15 +39958,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: @@ -40025,7 +40025,7 @@ snapshots: 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 @@ -40035,12 +40035,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 @@ -40048,27 +40048,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 @@ -40084,7 +40084,7 @@ snapshots: 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: @@ -40104,7 +40104,7 @@ snapshots: 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: @@ -40119,11 +40119,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: @@ -40138,11 +40138,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: @@ -40157,22 +40157,22 @@ 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.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.12 - '@vitest/mocker': 4.1.12(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)) '@vitest/pretty-format': 4.1.12 '@vitest/runner': 4.1.12 '@vitest/snapshot': 4.1.12 @@ -40189,7 +40189,7 @@ 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 @@ -40213,7 +40213,7 @@ snapshots: 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.12 - '@vitest/mocker': 4.1.12(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)) '@vitest/pretty-format': 4.1.12 '@vitest/runner': 4.1.12 '@vitest/snapshot': 4.1.12 @@ -40230,7 +40230,7 @@ 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 @@ -40396,16 +40396,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 @@ -40429,13 +40429,13 @@ 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)) + 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 @@ -40450,16 +40450,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 @@ -40473,8 +40473,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 @@ -40497,7 +40497,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 @@ -40511,8 +40511,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 @@ -40534,7 +40534,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 @@ -40557,7 +40557,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: From 6130d5a76a493bfee379cfca67600e6dd8ac80b5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Nov 2026 15:07:46 +0100 Subject: [PATCH 83/92] test(replay): Collect request spans across recording segments (#26209) **Why:** The tests only checked Replay segments 0 and 1, but asynchronous response processing can put the fetch/XHR span in a later segment. Collect segments until the expected request span arrives, then assert its full contents and require exactly one match. Co-authored-by: GPT-6 --- .../suites/replay/requests/test.ts | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) 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]); }); From 357737095d91f6202de1f24839701ba7e58eec9b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 10 Nov 2026 09:09:59 -0500 Subject: [PATCH 84/92] feat(tanstackstart-react): Deprecate `tunnelRoute` (#26225) `tunnelRoute` gets replaced by `tunnel: true` with `serveTunnelIntegration()` and goes away in v12, so I deprecated it with a JSDoc tag and a one-time build warning, like Next.js. I added a test that the warning shows once, and only when `tunnelRoute` is set. --- .../src/vite/sentryTanstackStart.ts | 21 ++++++++++++++++--- .../test/vite/sentryTanstackStart.test.ts | 19 +++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) 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/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', () => { From 39b10d84fbbb3c19ef80734b15ff5fe1b00ee8ab Mon Sep 17 00:00:00 2001 From: Nicolas Untz Date: Tue, 10 Nov 2026 07:14:09 -0700 Subject: [PATCH 85/92] fix(cloudflare): Keep framework-managed methods when a Durable Object constructor throws (#25851) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** When a Durable Object's constructor throws after a framework has already installed its method wrappers, the next construction reads those wrappers as the originals. Nothing then looks framework-managed, so Sentry wraps them again, and the framework's identity-keyed dispatch breaks. With the `agents` SDK, `@callable()` methods are wrapped before storage initialization, so a constructor that fails once (for example at a storage quota limit) leaves later RPCs failing with `Method callTool is not callable.` The fix keeps the methods collected before the first construction attempt until a construction succeeds, then drops them; after that, the existing `frameworkManagedMethodsCache` takes over. **How to review:** `resolveMethodsBeforeFirstConstruction` in `durableobject.ts`, plus the new test in the `framework-managed methods` block, which fails on `develop`. **Unsure about:** I keep the snapshot until the first *successful* construction rather than caching the result of a failed one. A framework that installs its wrappers on a later attempt would still be detected. The same change, applied as a `pnpm patch` to 11.4.0, fixes the failure in our app's Agent recovery test. Closes #25728 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: JPeer264 --- .../cloudflare-agent/tests/agent-socket.ts | 33 ++++++++++------ .../tests/constructor-failure.test.ts | 17 ++++++++ .../worker-configuration.d.ts | 3 +- .../cloudflare-agent/worker/index.ts | 22 +++++++++++ .../cloudflare-agent/wrangler.jsonc | 5 ++- packages/cloudflare/src/durableobject.ts | 25 +++++++++++- .../cloudflare/test/durableobject.test.ts | 39 +++++++++++++++++++ 7 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/constructor-failure.test.ts 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/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 541b507a7d56..89a1f1e155f0 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -63,7 +63,9 @@ 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 @@ -85,6 +87,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 +162,7 @@ function resolveFrameworkManagedMethods( } frameworkManagedMethodsCache.set(prototype, managed); + methodsBeforeFirstConstructionCache.delete(prototype); return managed; } diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index 6f7e2f47744a..ce02c45e9ffd 100644 --- a/packages/cloudflare/test/durableobject.test.ts +++ b/packages/cloudflare/test/durableobject.test.ts @@ -633,6 +633,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 From 312de2ea039b4017dbcae21a10cf139589ffc19e Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:16:58 +0000 Subject: [PATCH 86/92] chore: use native `node:fs` for removal scripts (#26219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** Part of #26187. Barely any of rimraf's functionality is used in this repo, so its just acting as a convenience wrapper to let us call it via CLI. We can achieve the same through a small script. **How to review:** Just ensure the scripts are consistent with each other and CI passes. **Unsure about:** N/A @andreiborza it actually turns out the bundler-plugin-integration-tests already use `premove` in `develop` πŸ‘€ --- .../browser-integration-tests/package.json | 2 +- .../scripts/clean.mjs | 5 +++++ .../package.json | 5 ++--- dev-packages/e2e-tests/package.json | 7 ++----- .../scripts/clean-test-applications.mjs | 8 ++++++++ .../node-integration-tests/package.json | 3 +-- .../node-integration-tests/scripts/clean.js | 7 ++++++- dev-packages/test-utils/package.json | 2 +- package.json | 7 +++---- packages/angular/package.json | 2 +- packages/astro/package.json | 2 +- packages/aws-serverless/package.json | 4 ++-- packages/browser-utils/package.json | 2 +- packages/browser/package.json | 2 +- packages/bun/package.json | 2 +- packages/bundler-plugins/package.json | 4 ++-- packages/cloudflare/package.json | 2 +- packages/core/package.json | 2 +- packages/deno/package.json | 2 +- packages/effect/package.json | 2 +- packages/elysia/package.json | 2 +- packages/ember/package.json | 2 +- packages/esbuild-plugin/package.json | 2 +- packages/eslint-config-sdk/package.json | 2 +- packages/eslint-plugin-sdk/package.json | 2 +- packages/feedback/package.json | 2 +- packages/gatsby/package.json | 2 +- packages/google-cloud-serverless/package.json | 2 +- packages/hono/package.json | 2 +- packages/integration-shims/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nitro/package.json | 2 +- packages/node/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/opentelemetry/package.json | 2 +- packages/react-router/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/replay-canvas/package.json | 2 +- packages/replay-internal/package.json | 2 +- packages/replay-worker/package.json | 2 +- packages/rollup-plugin/package.json | 2 +- .../server-runtime-injection/package.json | 2 +- packages/server-utils/package.json | 2 +- packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/typescript/package.json | 2 +- packages/vercel-edge/package.json | 2 +- packages/vite-plugin/package.json | 2 +- packages/vue/package.json | 2 +- packages/wasm/package.json | 2 +- packages/webpack-plugin/package.json | 2 +- pnpm-lock.yaml | 19 +------------------ scripts/rm.mjs | 5 +++++ 58 files changed, 84 insertions(+), 84 deletions(-) create mode 100644 dev-packages/browser-integration-tests/scripts/clean.mjs create mode 100644 dev-packages/e2e-tests/scripts/clean-test-applications.mjs create mode 100644 scripts/rm.mjs diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 542cc570aede..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", 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/bundler-plugin-integration-tests/package.json b/dev-packages/bundler-plugin-integration-tests/package.json index 71199d0ff14a..7969c5ea72d3 100644 --- a/dev-packages/bundler-plugin-integration-tests/package.json +++ b/dev-packages/bundler-plugin-integration-tests/package.json @@ -8,12 +8,11 @@ "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.49.0", "typescript": "~6.0.3", "vitest": "^4.1.12" 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/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 659ab71d5fa8..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", 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/test-utils/package.json b/dev-packages/test-utils/package.json index 8064e435ebe1..5af39e269001 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -55,7 +55,7 @@ "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" diff --git a/package.json b/package.json index 92fcf96fc6b7..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", @@ -160,7 +160,6 @@ "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", diff --git a/packages/angular/package.json b/packages/angular/package.json index debd43171053..b42f359ad95b 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -51,7 +51,7 @@ "build:transpile:watch": "ng build --configuration production --watch", "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 fe40f775f0dc..2e12f87e3a60 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -80,7 +80,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-astro-*.tgz", + "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/aws-serverless/package.json b/packages/aws-serverless/package.json index 61fb750a8fec..e41615d95242 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -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", @@ -91,7 +91,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/browser-utils/package.json b/packages/browser-utils/package.json index 20eeec3ef11c..3e6a1aca9802 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -49,7 +49,7 @@ "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-browser-utils-*.tgz", + "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/package.json b/packages/browser/package.json index 267d484a3a42..52864ae1b7d7 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -62,7 +62,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/bun/package.json b/packages/bun/package.json index 825b9bc34cc3..db2ff14d7e2e 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -63,7 +63,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/bundler-plugins/package.json b/packages/bundler-plugins/package.json index 0fb3dcc6d30e..817dd32df5d8 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -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", diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index b4e3ffbe6e9d..45c71352910f 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -93,7 +93,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/core/package.json b/packages/core/package.json index aa8ad4ee798b..f01c6b16c80e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -77,7 +77,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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", diff --git a/packages/deno/package.json b/packages/deno/package.json index 148de4354e80..2bbdd0450b69 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -44,7 +44,7 @@ "build:types": "tsc -p tsconfig.types.json", "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/effect/package.json b/packages/effect/package.json index c6b742b3d7e5..474eaf41bcac 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -72,7 +72,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-effect-*.tgz", + "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 65c2df11d844..ad8d65780d1f 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -59,7 +59,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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 657e11d744d0..779dde1cc708 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -31,7 +31,7 @@ "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", diff --git a/packages/esbuild-plugin/package.json b/packages/esbuild-plugin/package.json index 93028b0103a0..abdd3c4fd5a3 100644 --- a/packages/esbuild-plugin/package.json +++ b/packages/esbuild-plugin/package.json @@ -41,7 +41,7 @@ "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-esbuild-plugin-*.tgz", + "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 c6eb7e52bb39..8920272e1dea 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -39,7 +39,7 @@ "eslint": "8.57.0" }, "scripts": { - "clean": "pnpm exec rimraf sentry-eslint-config-sdk-*.tgz", + "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 546a110abcf5..5cfe013780df 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -26,7 +26,7 @@ "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", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 3a7c9e2353ec..fb93ba3d96b7 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -51,7 +51,7 @@ "build:bundle:watch": "pnpm run build:bundle --watch", "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 707aa6d5e701..26031bda2b06 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -64,7 +64,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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 f41fd2a95648..d3460a70747f 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -68,7 +68,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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 2a889561520b..82fc5547f79f 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -123,7 +123,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/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 76edfe2eedc4..c6a59ebe66da 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -82,7 +82,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts src/setup.ts", - "clean": "rimraf build coverage sentry-nestjs-*.tgz ./*.d.ts ./*.d.ts.map", + "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 1c222d68dfbc..f7f4e6ac0a8b 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -118,7 +118,7 @@ "build:transpile:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/edge/index.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-nextjs-*.tgz", + "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/nitro/package.json b/packages/nitro/package.json index c72ee4a0c490..979cdca2f177 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -59,7 +59,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-nitro-*.tgz", + "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/node/package.json b/packages/node/package.json index 9264093ed787..a50468cc5743 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -99,7 +99,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/nuxt/package.json b/packages/nuxt/package.json index 9dd47e0ccbb8..13599ef3bbf3 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -90,7 +90,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-nuxt-*.tgz", + "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/opentelemetry/package.json b/packages/opentelemetry/package.json index 9378ec0b381b..fd3bf2536f2f 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -47,7 +47,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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/react-router/package.json b/packages/react-router/package.json index c495545c25fd..9c69f5041ddc 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -93,7 +93,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-react-router-*.tgz", + "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/package.json b/packages/react/package.json index 7645bcdc97db..1acbd7615376 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -98,7 +98,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts src/react-router.ts", - "clean": "rimraf build coverage sentry-react-*.tgz", + "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 2ffe8415176a..7f02104fe54d 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -117,7 +117,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.server.ts src/index.client.ts", - "clean": "rimraf build coverage sentry-remix-*.tgz", + "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/replay-canvas/package.json b/packages/replay-canvas/package.json index c73027c08762..7c858cae1935 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -35,7 +35,7 @@ "build:bundle:watch": "pnpm run build:bundle --watch", "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", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index cb57d5650c9b..a6e838dcca40 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -49,7 +49,7 @@ "build:bundle:watch": "pnpm run build:bundle --watch", "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", 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 68b1a58eb913..9f6dbc283144 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -42,7 +42,7 @@ "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-rollup-plugin-*.tgz", + "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 34f71da49740..2dbe809e1279 100644 --- a/packages/server-runtime-injection/package.json +++ b/packages/server-runtime-injection/package.json @@ -66,7 +66,7 @@ "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-server-runtime-injection-*.tgz", + "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 f230c704670e..5f952d76c334 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -123,7 +123,7 @@ "build:dev:watch": "run-p build:transpile:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-server-utils-*.tgz", + "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/solid/package.json b/packages/solid/package.json index d57429c2a6bb..9b1ae0a11677 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -95,7 +95,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts src/solidrouter.ts src/tanstackrouter.ts", - "clean": "rimraf build coverage sentry-solid-*.tgz ./*.d.ts ./*.d.ts.map", + "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 2e068dc16ef0..4bae8eb75475 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -117,7 +117,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", + "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/svelte/package.json b/packages/svelte/package.json index 576fae116d97..8fe811e474fa 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -57,7 +57,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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 33d5b695eb5e..be05695b7451 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -103,7 +103,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-sveltekit-*.tgz", + "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/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index acbd60baba9c..cca63a18f138 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -76,7 +76,7 @@ "build:transpile:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.client.ts src/index.server.ts src/index.types.ts", - "clean": "rimraf build coverage sentry-tanstackstart-react-*.tgz", + "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/typescript/package.json b/packages/typescript/package.json index dd20991b732e..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", + "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 2348a9c34b89..d6cf352395ee 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -51,7 +51,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "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 78287002881a..371c338fe7db 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -41,7 +41,7 @@ "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-vite-plugin-*.tgz", + "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 b100c6278401..d6d351d77373 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -78,7 +78,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", "circularDepCheck": "madge --circular src/index.ts src/tanstackrouter.ts", - "clean": "rimraf build coverage sentry-vue-*.tgz", + "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 4534be38c341..6afdabd89b4e 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -52,7 +52,7 @@ "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 090be0093a7b..a152977b26b6 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -46,7 +46,7 @@ "build:dev:watch": "pnpm run build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:tarball": "pnpm pack", - "clean": "rimraf build coverage sentry-webpack-plugin-*.tgz", + "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 9df2789b99bd..96660d04a6d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -387,9 +384,6 @@ importers: '@sentry/bundler-plugins': specifier: 11.11.0 version: link:../../packages/bundler-plugins - premove: - specifier: ^4.0.0 - version: 4.0.0 sentry: specifier: ^0.49.0 version: 0.49.0 @@ -571,11 +565,7 @@ importers: 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: @@ -16045,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'} @@ -37156,8 +37141,6 @@ snapshots: prelude-ls@1.2.1: {} - premove@4.0.0: {} - prettier@2.8.8: {} prettier@3.9.10: {} 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 }); +} From 2fe81bb5289784fc7be7d4c809ffdf9d854ce6f3 Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:17:59 +0100 Subject: [PATCH 87/92] chore: Add external contributor to CHANGELOG.md (#26244) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #25851 Co-authored-by: JPeer264 <10677263+JPeer264@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a81c317c10..caa6cb979ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, and @chan-mai. Thank you for your contributions! +Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, @chan-mai, and @nuntz. Thank you for your contributions! ### Important Changes From d320067909c4d6ad840cfa7e480fa07596a3d87e Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:22:06 +0100 Subject: [PATCH 88/92] chore: Add external contributor to CHANGELOG.md (#26245) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #26219 Co-authored-by: andreiborza <168741329+andreiborza@users.noreply.github.com> Co-authored-by: Martin Sonnberger --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caa6cb979ae6..94cb6dcefa9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, @chan-mai, and @nuntz. Thank you for your contributions! +Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, @chan-mai, @nuntz, and @43081j. Thank you for your contributions! ### Important Changes From 83f29b739417174b6d0ec86a1e5f13453f262894 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Nov 2026 15:22:34 +0100 Subject: [PATCH 89/92] perf(ci): cache WebKit system package downloads (#26198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Why:** We recently reduced browser system-package downloads by an estimated 6GB per full CI run. WebKit still needs about 29MB per job, and those remaining downloads can occasionally take over 15 minutes (recently happened in around 10-15% of runs). This change caches the installers so warm jobs fetch them from GitHub’s cache instead of Ubuntu mirrors reducing the risk of hitting slow downloads. The cache is saved on develop and then restored on PR runs. --------- Co-authored-by: OpenAI Codex --- .github/actions/install-playwright/action.yml | 93 +++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index 7dfc5482857d..b642f77815d3 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -46,27 +46,88 @@ runs: shell: bash working-directory: ${{ runner.temp }} - - name: Install headless WebKit system dependencies + - name: Resolve headless WebKit system dependencies + id: webkit-dependencies if: steps.playwright-version.outputs.version != '' && contains(inputs.browsers, 'webkit') run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - 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 \ + 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 + - 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 uses: actions/cache/save@v6.1.0 From a6b06adee4ed91b03e33ad536c8449fc09c12419 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:44:37 +0100 Subject: [PATCH 90/92] fix(cloudflare): Initialize the SDK before constructing a Durable Object (#26218) ## What The Durable Object wrapper now starts the SDK before the constructor runs. A pi-durable Harness opened in the constructor is now traced. ## Why In an isolate where no Worker handler had called `init()` yet, the Harness opened before the pi-durable integration subscribed, so no agent, chat or tool spans were sent. --------- Co-authored-by: Claude Opus 5.5 --- .../constructor-async-work/test.ts | 7 +- .../cloudflare-pi-durable/src/eager.ts | 75 +++++++++++++++++++ .../cloudflare-pi-durable/src/env.d.ts | 1 + .../cloudflare-pi-durable/src/index.ts | 4 + .../tests/pi-durable.test.ts | 29 ++++++- .../cloudflare-pi-durable/tests/utils.ts | 25 +++++++ .../cloudflare-pi-durable/vite.config.mts | 2 +- .../wrangler.eager.jsonc | 12 +++ .../cloudflare-pi-durable/wrangler.jsonc | 18 +++++ .../cloudflare-pi-durable/wrangler.toml | 18 ----- packages/cloudflare/src/durableobject.ts | 16 +++- .../cloudflare/test/durableobject.test.ts | 21 ++++++ 12 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/src/eager.ts create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.eager.jsonc create mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.jsonc delete mode 100644 dev-packages/e2e-tests/test-applications/cloudflare-pi-durable/wrangler.toml 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/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/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 89a1f1e155f0..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'; @@ -70,9 +72,17 @@ export function constructInstrumentedDurableObject // 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, diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts index ce02c45e9ffd..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[] = []; From 0f3145f76c4c8057ede8acc4a55901babbf144cf Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Nov 2026 15:51:07 +0100 Subject: [PATCH 91/92] fix(ci): prune unused rimraf lockfile entries (#26247) **Why:** Removing rimraf in #26219 left two unused lockfile entries that pnpm prunes during installation, causing develop's lockfile stability check to fail. Commit the generated cleanup to restore CI. Co-authored-by: OpenAI Codex --- pnpm-lock.yaml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96660d04a6d1..cf8b8ac67374 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16612,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} @@ -37827,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 From 7be0b7a822b7fdcef7025e83e28f761ce920dd2e Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 10 Nov 2026 16:00:26 +0100 Subject: [PATCH 92/92] meta(changelog): Update changelog for 11.12.0 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cb6dcefa9b..cae8ddfbdbb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. β€” Wayne Gretzky" β€” Michael Scott -Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale190895, @arthurfiorette, @chan-mai, @nuntz, and @43081j. Thank you for your contributions! +## 11.12.0 ### Important Changes @@ -23,6 +23,114 @@ Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale1908 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