diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index a862cd355af2..91374c88208f 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -15,7 +15,7 @@ runs: shell: bash - name: Check dependency cache - uses: actions/cache@v5 + uses: actions/cache@v6.1.0 id: cache_dependencies with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} diff --git a/.github/actions/install-playwright/action.yml b/.github/actions/install-playwright/action.yml index ec6ae171e925..0422128daff3 100644 --- a/.github/actions/install-playwright/action.yml +++ b/.github/actions/install-playwright/action.yml @@ -18,7 +18,7 @@ runs: working-directory: ${{ inputs.cwd }} - name: Restore cached playwright binaries - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6.1.0 id: playwright-cache with: path: | @@ -43,7 +43,7 @@ runs: # Only store cache on develop branch - name: Store cached playwright binaries - uses: actions/cache/save@v5 + uses: actions/cache/save@v6.1.0 if: github.event_name == 'push' && github.ref == 'refs/heads/develop' with: path: | diff --git a/.github/actions/restore-cache/action.yml b/.github/actions/restore-cache/action.yml index 1d5126fbe952..ce5c2abd676e 100644 --- a/.github/actions/restore-cache/action.yml +++ b/.github/actions/restore-cache/action.yml @@ -11,7 +11,7 @@ runs: steps: - name: Check dependency cache id: dep-cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6.1.0 with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ inputs.dependency_cache_key }} diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index a248ca5b0756..03b8f9048b52 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -51,7 +51,7 @@ jobs: node-version-file: 'package.json' - name: Prepare release - uses: getsentry/craft@cdb657d4bbc70cd497876ad158984b4d345a48ae # v2.26.14 + uses: getsentry/craft@aeb16753a1764f3ef0768c03c499e3d2e4b7227c # v2.28.0 if: github.event.pull_request.merged == true && steps.version-regex.outputs.match != '' && steps.get_version.outputs.version != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ad4d82fd9d6..80b921b818bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,7 +121,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Store NX cache - uses: actions/cache/save@v5 + uses: actions/cache/save@v6.1.0 # Only cache this per-PR to speed up CI. if: github.event_name == 'pull_request' with: @@ -206,7 +206,7 @@ jobs: with: node-version-file: 'package.json' - name: Restore NX cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6.1.0 with: path: | .nx/cache @@ -245,7 +245,7 @@ jobs: with: node-version-file: 'package.json' - name: Restore NX cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6.1.0 with: path: | .nx/cache @@ -968,7 +968,7 @@ jobs: with: node-version-file: 'package.json' - name: Restore NX cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6.1.0 with: path: | .nx/cache diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 414cd48d9245..c20b80e9f71c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -39,7 +39,7 @@ jobs: with: node-version-file: 'package.json' - name: Check canary cache - uses: actions/cache@v5 + uses: actions/cache@v6.1.0 with: path: ${{ env.CACHED_BUILD_PATHS }} key: canary-${{ env.HEAD_COMMIT }} @@ -132,7 +132,7 @@ jobs: node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Restore canary cache - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6.1.0 with: path: ${{ env.CACHED_BUILD_PATHS }} key: canary-${{ env.HEAD_COMMIT }} diff --git a/.github/workflows/ci-metadata.yml b/.github/workflows/ci-metadata.yml index 39be39fc5339..4c3cdbb44f3e 100644 --- a/.github/workflows/ci-metadata.yml +++ b/.github/workflows/ci-metadata.yml @@ -64,7 +64,7 @@ jobs: # Most changed packages are determined in job_build via Nx - name: Determine changed packages - uses: dorny/paths-filter@v4.0.1 + uses: dorny/paths-filter@v4.0.2 id: changed with: filters: | diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index ecff5c4dff38..fe84a4085238 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -44,7 +44,7 @@ jobs: browsers: 'chromium' - name: Determine changed tests - uses: dorny/paths-filter@v4.0.1 + uses: dorny/paths-filter@v4.0.2 id: changed with: list-files: json diff --git a/.github/workflows/pr-stale-notifier.yml b/.github/workflows/pr-stale-notifier.yml index 2b5761936773..13bba260a0b0 100644 --- a/.github/workflows/pr-stale-notifier.yml +++ b/.github/workflows/pr-stale-notifier.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Warn and close stale PRs - uses: actions/stale@v10 + uses: actions/stale@v11 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-issue-stale: -1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4914d6e42dde..bff8d19e8cd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: with: node-version-file: 'package.json' - name: Prepare release - uses: getsentry/craft@cdb657d4bbc70cd497876ad158984b4d345a48ae # v2.26.14 + uses: getsentry/craft@aeb16753a1764f3ef0768c03c499e3d2e4b7227c # v2.28.0 env: GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: diff --git a/.size-limit.js b/.size-limit.js index a658081c4bf0..f70e9927fb13 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -432,7 +432,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '200 KiB', + limit: '230 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { @@ -452,7 +452,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '482 KiB', + limit: '540 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { diff --git a/MIGRATION.md b/MIGRATION.md index 4649819063d1..dbae6511e3a5 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -526,6 +526,38 @@ Remove these options from your `next.config.js` / `next.config.ts`. The deprecated `sourceMapsUploadOptions` and other deprecated Vite/build plugin options were removed from `@sentry/astro`, `@sentry/nuxt`, `@sentry/sveltekit`, and `@sentry/react-router`. Use the top-level equivalents (e.g. `sourcemaps`, `release`, `authToken`, `org`, `project`, `telemetry`) instead. +### `@sentry/nuxt` + +The deprecated `sourceMapsUploadOptions` module option was removed. Move its fields to the root level of the `sentry` module options. Note that `url` was renamed to `sentryUrl`, and `enabled` was replaced by `sourcemaps.disable` (inverted: `enabled: false` becomes `sourcemaps: { disable: true }`). + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@sentry/nuxt/module'], + sentry: { + // before + sourceMapsUploadOptions: { + org: 'my-org', + project: 'my-project', + authToken: process.env.SENTRY_AUTH_TOKEN, + url: 'https://my-sentry.example.com', + sourcemaps: { + assets: ['./dist/**/*'], + }, + }, + + // after + org: 'my-org', + project: 'my-project', + authToken: process.env.SENTRY_AUTH_TOKEN, + sentryUrl: 'https://my-sentry.example.com', + sourcemaps: { + assets: ['./dist/**/*'], + }, + }, +}); +``` + ## 4. Package Removals ### `@sentry/types` is no longer published diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/basic/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/d1/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/d1/wrangler.jsonc index cad8622a6c77..9ebec402e521 100644 --- a/dev-packages/cloudflare-integration-tests/suites/d1/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/d1/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "d1-test-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "d1_databases": [ { "binding": "DB", diff --git a/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/double-instrumentation/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-disabled/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-disabled/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-disabled/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-disabled/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-filtered/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-filtered/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-filtered/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-filtered/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-ignore/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-ignore/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-ignore/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-ignore/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-small/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-small/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-small/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server-small/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts b/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts index 631e2cea18b2..64663e9e1838 100644 --- a/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/index.ts @@ -1,6 +1,6 @@ import type { D1Database } from '@cloudflare/workers-types'; import { PrismaD1 } from '@prisma/adapter-d1'; -import * as Sentry from '@sentry/cloudflare/nodejs_compat'; +import * as Sentry from '@sentry/cloudflare'; import { PrismaClient } from './generated'; interface Env { diff --git a/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts b/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts index 2a4c259b4058..ad1e986843b1 100644 --- a/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/prisma/test.ts @@ -18,7 +18,7 @@ function envelopeItem(envelope: Envelope): Record { return envelope[1][0]![1] as Record; } -it('captures a transaction with Prisma spans for a D1 query via the @sentry/cloudflare/nodejs_compat prismaIntegration', async ({ +it('captures a transaction with Prisma spans for a D1 query via the @sentry/cloudflare prismaIntegration', async ({ signal, }) => { const runner = createRunner(__dirname) diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/metrics/server-address/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/wrangler.jsonc index 459b311911d5..b247aa82fb26 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan-streamed/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "start-span-streamed", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/queue/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/queue/wrangler.jsonc index 869aa21e0ec3..d731714bffe0 100644 --- a/dev-packages/cloudflare-integration-tests/suites/queue/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/queue/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "queues": { "producers": [ { diff --git a/dev-packages/cloudflare-integration-tests/suites/r2/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/r2/wrangler.jsonc index 88bd42c3a7df..e177f1f793ec 100644 --- a/dev-packages/cloudflare-integration-tests/suites/r2/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/r2/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "r2_buckets": [ { "binding": "MY_BUCKET", diff --git a/dev-packages/cloudflare-integration-tests/suites/ratelimit/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/ratelimit/wrangler.jsonc index 97ed092cdc9b..180d552f39b8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/ratelimit/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/ratelimit/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "ratelimits": [ { "name": "MY_RATE_LIMITER", diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/index.ts b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/index.ts new file mode 100644 index 000000000000..852fe0a34fd9 --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/index.ts @@ -0,0 +1,25 @@ +import { wrapRequestHandler } from '@sentry/cloudflare/request'; + +interface Env { + SENTRY_DSN: string; +} + +// Mirrors how Hydrogen (Remix) uses the SDK: only `wrapRequestHandler` from the +// `/request` subpath, without `nodejs_compat` compatibility flags. +// The subpath must stay free of Node.js-only modules. +export default { + async fetch(request, env, ctx) { + return wrapRequestHandler( + { + options: { + dsn: env.SENTRY_DSN, + traceLifecycle: 'static', + tracesSampleRate: 1, + }, + request, + context: ctx, + }, + () => new Response('ok'), + ); + }, +} satisfies ExportedHandler; diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/test.ts b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/test.ts new file mode 100644 index 000000000000..b55df04197ff --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/test.ts @@ -0,0 +1,18 @@ +import { expect, it } from 'vitest'; +import { createRunner } from '../../../runner'; + +it('captures a transaction via wrapRequestHandler from the /request subpath without node compatibility flags', async ({ + signal, +}) => { + const runner = createRunner(__dirname) + .expect(envelope => { + const transactionEvent = envelope[1]?.[0]?.[1] as any; + expect(transactionEvent.transaction).toBe('GET /'); + expect(transactionEvent.contexts?.trace?.op).toBe('http.server'); + expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.cloudflare'); + }) + .start(signal); + + await runner.makeRequest('get', '/'); + await runner.completed(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/wrangler.jsonc new file mode 100644 index 000000000000..8e2b7634629e --- /dev/null +++ b/dev-packages/cloudflare-integration-tests/suites/request-handler/subpath/wrangler.jsonc @@ -0,0 +1,5 @@ +{ + "name": "worker-name", + "compatibility_date": "2025-06-17", + "main": "index.ts", +} diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/wrangler.jsonc index d853a1899850..d4d4fdc02cb1 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "anthropic-ai-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/wrangler.jsonc index eba80524b05c..0ae1692d6726 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "d1-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "d1_databases": [ { "binding": "DB", diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/dsc-url-source/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/wrangler.jsonc index bf41cd5981e0..03b1e72c5146 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links-sync/wrangler.jsonc @@ -16,5 +16,5 @@ }, ], }, - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/wrangler.jsonc index e605296a46c5..455850051e9e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-alarm-links/wrangler.jsonc @@ -16,5 +16,5 @@ }, ], }, - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/wrangler.jsonc index 8a544e1bdf6b..1bb2073661eb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-spans/wrangler.jsonc @@ -16,5 +16,5 @@ }, ], }, - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/wrangler.jsonc index 8a544e1bdf6b..1bb2073661eb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/wrangler.jsonc @@ -16,5 +16,5 @@ }, ], }, - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/wrangler.jsonc index 8a544e1bdf6b..1bb2073661eb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/wrangler.jsonc @@ -16,5 +16,5 @@ }, ], }, - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc index 8a544e1bdf6b..1bb2073661eb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/wrangler.jsonc @@ -16,5 +16,5 @@ }, ], }, - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/wrangler.jsonc index 089f1332bd7a..695564b5f2be 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "google-genai-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/headers/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/headers/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/headers/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/headers/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/wrangler.jsonc index e0705205fde7..27e22023faa7 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-child/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "ignore-spans-streamed-continued-trace-child", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/wrangler.jsonc index bb4a0574d8e4..1ba5a8440a1f 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-http-client/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "ignore-spans-streamed-continued-trace-http-client", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/wrangler.jsonc index 762349725a01..21148fa9dfe0 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/ignoreSpans-streamed/continued-trace-segment/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "ignore-spans-streamed-continued-trace-segment", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/wrangler.jsonc index 28d4a0a81f19..3321c16a5b68 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/instrument-fetcher/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-instrument-fetcher", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["EchoHeadersDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/wrangler.jsonc index 25dbb603b7a2..133205dccd2e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "langchain-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc index 48aaa79409a3..d6be01281f0c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "worker-name", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/wrangler.jsonc index 409081a63462..b5322ea8d2e2 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "openai-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/wrangler.jsonc index b6dc58439427..83c6a339826c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/no-propagation-worker-do/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-durable-objects", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/wrangler.jsonc index 0711a1d68d37..c97597605b8e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc-disabled/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-worker-do-rpc-disabled", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/wrangler.jsonc index 3f909c489513..9dba9a09ccb1 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do-rpc/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-worker-do-rpc", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/wrangler.jsonc index b6dc58439427..83c6a339826c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-do/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-durable-objects", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler-sub-worker.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler-sub-worker.jsonc index 2ba9611e4c48..ddf7d2aec165 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler-sub-worker.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler-sub-worker.jsonc @@ -2,5 +2,5 @@ "name": "cloudflare-service-binding-sub-worker", "main": "index-sub-worker.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler.jsonc index a2d52620d268..edf0e3bacdeb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-service-binding/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-worker-service-binding", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "services": [ { "binding": "ANOTHER_WORKER", diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler-sub-worker.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler-sub-worker.jsonc index 063d8e9224ad..761486c5951d 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler-sub-worker.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler-sub-worker.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-worker-worker-do-rpc-sub", "main": "index-sub-worker.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler.jsonc index ddf9c607d906..33713d28595e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-worker-do-rpc/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-worker-worker-do-rpc", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "services": [ { "binding": "SUB_WORKER", diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler-sub-worker.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler-sub-worker.jsonc index 13de99007e1f..8757d2cddb81 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler-sub-worker.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler-sub-worker.jsonc @@ -2,5 +2,5 @@ "name": "cloudflare-worker-workerentrypoint-rpc-sub", "main": "index-sub-worker.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc index bd19f421dcff..badfcd962843 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/worker-workerentrypoint-rpc/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-worker-workerentrypoint-rpc", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als", "enable_ctx_exports"], + "compatibility_flags": ["nodejs_compat", "enable_ctx_exports"], "services": [ { "binding": "SUB_WORKER", diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/wrangler.jsonc index 78303e091bf4..b527f7c146c5 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc-disabled/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-workerentrypoint-do-rpc-disabled", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/wrangler.jsonc index e0d4024f8b8b..c41c9ebe00eb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-do-rpc/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-workerentrypoint-do-rpc", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler-sub-worker.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler-sub-worker.jsonc index 873f66317fc8..3b18e39650de 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler-sub-worker.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler-sub-worker.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-workerentrypoint-workerentrypoint-do-rpc-sub", "main": "index-sub-worker.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler.jsonc index 45dfacf580a1..c46bf6a5f11b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workerentrypoint-workerentrypoint-do-rpc/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-workerentrypoint-workerentrypoint-do-rpc", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "services": [ { "binding": "SUB_WORKER", diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/wrangler.jsonc index fd8a63daf3f5..14c2597a13cd 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/propagation/workflow-do/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-durable-objects", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "migrations": [ { "new_sqlite_classes": ["MyDurableObject"], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/wrangler.jsonc index 90cab6f2dd11..12630676aa01 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "scheduled-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "triggers": { "crons": ["* * * * *"], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/wrangler.jsonc deleted file mode 100644 index 5a451fde4ab6..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/wrangler.jsonc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "vercelai-v6-worker", - "compatibility_date": "2026-04-26", - "main": "index.ts", - "compatibility_flags": ["nodejs_als"], -} diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/index.ts deleted file mode 100644 index 09ccb1f9ce66..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as Sentry from '@sentry/cloudflare/nodejs_compat'; -import { generateText } from 'ai'; -import { MockLanguageModelV3 } from 'ai/test'; - -interface Env { - SENTRY_DSN: string; -} - -export default Sentry.withSentry( - (env: Env) => ({ - dsn: env.SENTRY_DSN, - traceLifecycle: 'static', - tracesSampleRate: 1, - integrations: [Sentry.vercelAIIntegration()], - }), - { - async fetch(_request, _env, _ctx) { - await generateText({ - experimental_telemetry: { isEnabled: true }, - 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: 'Hello from mock AI!' }], - warnings: [], - }), - // The mock result shape differs from this `ai` version's LanguageModelV3 result type; cast to the mock's expected config. - } as unknown as ConstructorParameters[0]), - prompt: 'Where is the first span?', - }); - - return new Response('ok'); - }, - }, -); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/test.ts deleted file mode 100644 index 1f961f202119..000000000000 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { expect, it } from 'vitest'; -import { - GEN_AI_OPERATION_NAME, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, -} from '@sentry/conventions/attributes'; -import { createRunner } from '../../../../runner'; - -it('captures a transaction with Vercel AI v6 spans via @sentry/cloudflare/nodejs_compat vercelAIIntegration', async ({ - signal, -}) => { - const runner = createRunner(__dirname) - .ignore('event') - .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1] as any; - expect(transactionEvent.transaction).toBe('GET /'); - - const container = envelope[1]?.[1]?.[1] as any; - expect(container).toBeDefined(); - - expect(container.items).toHaveLength(2); - expect(container.items).toEqual( - expect.arrayContaining([ - { - trace_id: expect.any(String), - span_id: expect.any(String), - parent_span_id: expect.any(String), - name: 'invoke_agent', - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - status: 'ok', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'gen_ai.invoke_agent' }, - 'sentry.origin': { type: 'string', value: 'auto.vercelai.otel' }, - [GEN_AI_OPERATION_NAME]: { type: 'string', value: 'invoke_agent' }, - [GEN_AI_USAGE_INPUT_TOKENS]: { type: 'integer', value: 10 }, - [GEN_AI_USAGE_OUTPUT_TOKENS]: { type: 'integer', value: 20 }, - [GEN_AI_USAGE_TOTAL_TOKENS]: { type: 'integer', value: 30 }, - }), - }, - { - trace_id: expect.any(String), - span_id: expect.any(String), - parent_span_id: expect.any(String), - name: 'generate_content mock-model-id', - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - status: 'ok', - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'gen_ai.generate_content' }, - 'sentry.origin': { type: 'string', value: 'auto.vercelai.otel' }, - [GEN_AI_OPERATION_NAME]: { type: 'string', value: 'generate_content' }, - }), - }, - ]), - ); - }) - .start(signal); - - await runner.makeRequest('get', '/'); - await runner.completed(); -}); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/index.ts similarity index 96% rename from dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/index.ts rename to dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/index.ts index 5b61bce9842c..f6129e046cf6 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/index.ts @@ -11,7 +11,6 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, traceLifecycle: 'static', tracesSampleRate: 1, - integrations: [Sentry.vercelAIIntegration()], }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/test.ts similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/als_v6/test.ts rename to dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/test.ts diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/wrangler.jsonc similarity index 100% rename from dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/compat_v6/wrangler.jsonc rename to dev-packages/cloudflare-integration-tests/suites/tracing/vercelai/v6/wrangler.jsonc diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc index 2cdb43e06500..11238eb68082 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/wrangler.jsonc @@ -2,5 +2,5 @@ "name": "workers-ai-worker", "compatibility_date": "2025-06-17", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/wrangler.jsonc index 1140a99ebe3f..b8d729d16591 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "workflow-worker", "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "workflows": [ { "name": "my-workflow", diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/wrangler.jsonc index 64b8c143d01b..bd838e1465a2 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-chained/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], // Self-service-binding: the worker binds to its own named `WorkerEntrypoint` // export, so the auto-instrument transform can identify and wrap it. "services": [ diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/wrangler.jsonc index 1f20fdfa5e0d..f08be236a28d 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do-manual-mixed/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], // Self-service-binding: the worker binds to its own named `WorkerEntrypoint` // export, so the auto-instrument transform can identify and wrap it. "services": [ diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/wrangler.jsonc index 8771cbe8153d..0977a08e255e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/combination-entrypoint-do/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], // Self-service-binding: the worker binds to its own named `WorkerEntrypoint` // export, so the auto-instrument transform can identify and wrap it. "services": [ diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/wrangler.jsonc index 13baff43e6b5..86adf4cd373f 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/default-export/wrangler.jsonc @@ -5,5 +5,5 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], } diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/wrangler.jsonc index d296b629350c..e750bdf964b0 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-manual-wrap/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/wrangler.jsonc index e9e39d2a3ed4..4c274d32e922 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-mixed/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ { "name": "MANUAL", "class_name": "Manual" }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/wrangler.jsonc index 25d19154d2a5..f65e927a3c68 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-multiple/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ { "name": "COUNTER_A", "class_name": "CounterA" }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/wrangler.jsonc index 11ff2a02a4a4..397b352cc730 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-reexport-instrumented/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/wrangler.jsonc index 68b8de16bff7..5de5b1094f1c 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier-alias/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { // The binding references the *exported* alias (`Counter`), while the class // is declared locally as `CounterImpl` and exported via `export { ... as Counter }`. diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/wrangler.jsonc index 86517a797c6d..45a5e07f1255 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-specifier/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/wrangler.jsonc index cf099a123f2f..724feb3c5066 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-manual-mixed/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/wrangler.jsonc index 326b55d3eae2..afd829c70e89 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow-specifier/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/wrangler.jsonc index 87cd01f3cc44..dc2e23a16462 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject-workflow/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/wrangler.jsonc index 681e81cbc0be..91b055ccd468 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/durableobject/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }], }, diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/wrangler.jsonc index 011471c55e9e..93c48530a9fb 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workerentrypoint/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], // Self-service-binding: the worker binds to its own named `WorkerEntrypoint` // export, so the auto-instrument transform can identify and wrap it. "services": [ diff --git a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/wrangler.jsonc index d21aa21a4659..92664bcfb386 100644 --- a/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/vite-autoinstrument/workflow/wrangler.jsonc @@ -5,7 +5,7 @@ // the auto-instrument transform runs) and the runner serves the built output. "main": "index.ts", "compatibility_date": "2025-06-17", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "workflows": [ { "name": "my-workflow", diff --git a/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/wrangler.jsonc index aab8853cf7c2..2c82218469d7 100644 --- a/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/wrangler.jsonc +++ b/dev-packages/cloudflare-integration-tests/suites/workflows/step-context/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "workflow-step-context-test", "compatibility_date": "2026-05-01", "main": "index.ts", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "workflows": [ { "name": "step-context-test-workflow", diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc index 135d0dbcd544..8ca6b10483d9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/wrangler.jsonc @@ -3,7 +3,7 @@ "name": "astro-5-cf-workers", "main": "dist/_worker.js/index.js", "compatibility_date": "2025-12-01", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "vars": { "SENTRY_DSN": "https://username@domain/123", "SENTRY_ENVIRONMENT": "qa", diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/wrangler.jsonc index aeabb81d7c07..eed026ecf585 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/wrangler.jsonc @@ -1,6 +1,6 @@ { "compatibility_date": "2026-03-12", - "compatibility_flags": ["global_fetch_strictly_public", "nodejs_als"], + "compatibility_flags": ["global_fetch_strictly_public", "nodejs_compat"], "name": "astro-6-cf-workers", "main": "./sentry.server.config.js", "assets": { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/wrangler.toml index c3052858b19c..96788a17d4c0 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-local-workers/wrangler.toml @@ -2,7 +2,7 @@ name = "cloudflare-local-workers" main = "src/index.ts" compatibility_date = "2024-07-25" -compatibility_flags = ["nodejs_als"] +compatibility_flags = ["nodejs_compat"] # [vars] # E2E_TEST_DSN = "" diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/package.json deleted file mode 100644 index f2757ef2b0f4..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "cloudflare-vercelai-v7-als", - "version": "0.0.0", - "private": true, - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", - "build": "wrangler deploy --dry-run", - "typecheck": "tsc --noEmit", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test:prod", - "test:prod": "TEST_ENV=production playwright test", - "test:dev": "TEST_ENV=development playwright test" - }, - "dependencies": { - "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", - "ai": "^7.0.0" - }, - "devDependencies": { - "@playwright/test": "~1.56.0", - "@cloudflare/workers-types": "^4.20240725.0", - "@sentry-internal/test-utils": "link:../../../test-utils", - "typescript": "^5.5.2", - "wrangler": "^4.61.0", - "ws": "^8.18.3" - }, - "volta": { - "node": "24.15.0", - "extends": "../../package.json" - }, - "pnpm": { - "overrides": { - "strip-literal": "~2.0.0" - } - } -} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts deleted file mode 100644 index aa4d046da5b3..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as Sentry from '@sentry/cloudflare'; -import { generateText } from 'ai'; -import { MockLanguageModelV3 } from 'ai/test'; - -export default Sentry.withSentry( - (env: Env) => ({ - traceLifecycle: 'static', - dsn: env.E2E_TEST_DSN, - environment: 'qa', - tunnel: 'http://localhost:3031/', - tracesSampleRate: 1.0, - dataCollection: { genAI: { inputs: false, outputs: false } }, - integrations: [Sentry.vercelAIIntegration()], - }), - { - async fetch(request, _env, _ctx) { - const url = new URL(request.url); - - if (url.pathname === '/generate') { - await generateText({ - experimental_telemetry: { isEnabled: true }, - 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: 'Hello from mock AI!' }], - warnings: [], - }), - }), - prompt: 'Where is the first span?', - }); - - return new Response('ok'); - } - - return new Response('Not found', { status: 404 }); - }, - } satisfies ExportedHandler, -); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/start-event-proxy.mjs deleted file mode 100644 index 69fc4ec92ccf..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'cloudflare-vercelai-v7-als', -}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts deleted file mode 100644 index 078452236a44..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tests/index.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForRequest } from '@sentry-internal/test-utils'; - -test('does not capture Vercel AI v7 spans without nodejs_compat', async ({ baseURL }) => { - // The transaction envelope also carries any extracted gen_ai spans as a span v2 container item, - // so we wait for the whole envelope and assert neither place contains AI spans. - const envelopePromise = waitForRequest('cloudflare-vercelai-v7-als', ({ envelope }) => { - const transactionItem = envelope[1].find(([header]) => header.type === 'transaction'); - return (transactionItem?.[1] as any)?.transaction === 'GET /generate'; - }); - - const response = await fetch(`${baseURL}/generate`); - expect(response.status).toBe(200); - - const { envelope } = await envelopePromise; - - const transaction = envelope[1].find(([header]) => header.type === 'transaction')?.[1] as any; - expect(transaction.transaction).toBe('GET /generate'); - expect(transaction.contexts?.trace?.op).toBe('http.server'); - - // v7 uses diagnostics_channel which is not available with nodejs_als, so no AI spans should be - // present — neither embedded in the transaction nor streamed as a span v2 container item. - const embeddedAiSpans = (transaction.spans || []).filter( - (span: any) => span.op?.startsWith('gen_ai.') || span.description?.includes('generateText'), - ); - expect(embeddedAiSpans).toHaveLength(0); - - const streamedGenAiSpans = envelope[1] - .filter(([header]) => header.type === 'span') - .flatMap(([, payload]) => (payload as any).items ?? []) - .filter((span: any) => getSpanOp(span)?.startsWith('gen_ai.')); - expect(streamedGenAiSpans).toHaveLength(0); -}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/wrangler.toml deleted file mode 100644 index eac01ce57058..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/wrangler.toml +++ /dev/null @@ -1,5 +0,0 @@ -#:schema node_modules/wrangler/config-schema.json -name = "cloudflare-vercelai-v7-als" -main = "src/index.ts" -compatibility_date = "2026-04-26" -compatibility_flags = ["nodejs_als"] diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/playwright.config.ts deleted file mode 100644 index 59a70bd39838..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/playwright.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; -const testEnv = process.env.TEST_ENV; - -if (!testEnv) { - throw new Error('No test env defined'); -} - -const APP_PORT = 38787; - -const config = getPlaywrightConfig( - { - startCommand: `pnpm dev --port ${APP_PORT}`, - port: APP_PORT, - }, - { - workers: '100%', - retries: 0, - }, -); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/env.d.ts deleted file mode 100644 index 7c5617be39aa..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/env.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface Env { - E2E_TEST_DSN: ''; -} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tsconfig.json deleted file mode 100644 index 28ac6a1d29b1..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "es2021", - "lib": ["es2021"], - "module": "es2022", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "allowJs": true, - "checkJs": false, - "noEmit": true, - "isolatedModules": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "types": ["@cloudflare/workers-types/experimental"] - }, - "exclude": ["test"], - "include": ["src/**/*.ts"] -} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/package.json rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/package.json diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/playwright.config.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/playwright.config.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/playwright.config.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/src/env.d.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/src/env.d.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/src/env.d.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/src/index.ts similarity index 92% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/src/index.ts index daae29d1dd53..a913252e9b22 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/src/index.ts @@ -1,4 +1,4 @@ -import * as Sentry from '@sentry/cloudflare/nodejs_compat'; +import * as Sentry from '@sentry/cloudflare'; import { generateText } from 'ai'; import { MockLanguageModelV3 } from 'ai/test'; @@ -10,7 +10,6 @@ export default Sentry.withSentry( tunnel: 'http://localhost:3031/', tracesSampleRate: 1.0, dataCollection: { genAI: { inputs: false, outputs: false } }, - integrations: [Sentry.vercelAIIntegration()], }), { async fetch(request, _env, _ctx) { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/start-event-proxy.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/start-event-proxy.mjs rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/start-event-proxy.mjs diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/tests/index.test.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/tests/index.test.ts rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/tests/index.test.ts diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/tsconfig.json similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-als/tsconfig.json rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/wrangler.toml similarity index 100% rename from dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7-compat/wrangler.toml rename to dev-packages/e2e-tests/test-applications/cloudflare-vercelai-v7/wrangler.toml diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml index b33fd47b128f..d86500477814 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-streaming/wrangler.toml @@ -2,7 +2,7 @@ name = "cloudflare-workers-streaming" main = "src/index.ts" compatibility_date = "2024-07-25" -compatibility_flags = ["nodejs_als"] +compatibility_flags = ["nodejs_compat"] # [vars] # E2E_TEST_DSN = "" diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-workflow-legacy/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-workers-workflow-legacy/wrangler.jsonc index c749c64192b0..1edde60ad28c 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-workflow-legacy/wrangler.jsonc +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-workflow-legacy/wrangler.jsonc @@ -2,7 +2,7 @@ "name": "cloudflare-workers-workflow-legacy", "main": "src/index.ts", "compatibility_date": "2025-05-01", - "compatibility_flags": ["nodejs_als"], + "compatibility_flags": ["nodejs_compat"], "workflows": [ { "name": "retry-test-workflow", diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts index 5ae618904b9a..285ae9d826ae 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/memory.test.ts @@ -34,7 +34,7 @@ test.describe('Worker V8 isolate memory tests', () => { // Compare batches to detect per-request leaks (excludes warm-up effects) const result = profiler.compareSnapshots(afterFirstBatch, afterSecondBatch); - expect(result.nodeGrowthPercent).toBeLessThan(0.25); + expect(result.nodeGrowthPercent).toBeLessThan(0.27); await profiler.close(); }); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml index 3feec54798a4..70b0ae034580 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml @@ -2,7 +2,7 @@ name = "cloudflare-workers" main = "src/index.ts" compatibility_date = "2024-07-25" -compatibility_flags = ["nodejs_als"] +compatibility_flags = ["nodejs_compat"] # [vars] # E2E_TEST_DSN = "" diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/wrangler.toml index f42e46753968..fe9a7e9c1204 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/wrangler.toml @@ -2,7 +2,7 @@ name = "cloudflare-workersentrypoint" main = "src/index.ts" compatibility_date = "2024-07-25" -compatibility_flags = ["nodejs_als"] +compatibility_flags = ["nodejs_compat"] [vars] E2E_TEST_DSN = "" diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash index a1831f1e8e76..4affbb553b0e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt-start-dev-server.bash @@ -25,8 +25,11 @@ if [ ! -f ".nuxt/dev/sentry.server.config.mjs" ]; then fi # 3. Cleanup +# `pkill -P` only kills direct children, so the grandchild dev server holding the +# port survives; newer Nuxt's directory-scoped dev lock then blocks the real start. echo "Found .nuxt/dev/sentry.server.config.mjs, stopping 'nuxt dev' process..." -pkill -P $DEV_PID || kill $DEV_PID +pkill -P $DEV_PID 2>/dev/null +kill $DEV_PID 2>/dev/null # Wait for port to be released echo "Waiting for port $TEMP_PORT to be released..." @@ -38,7 +41,13 @@ while lsof -i :$TEMP_PORT > /dev/null 2>&1 && [ $COUNTER -lt 10 ]; do done if lsof -i :$TEMP_PORT > /dev/null 2>&1; then - echo "WARNING: Port $TEMP_PORT still in use after 10 seconds, proceeding anyway..." + echo "Port $TEMP_PORT still in use, killing remaining processes bound to it..." + lsof -t -i :$TEMP_PORT | xargs -r kill -9 2>/dev/null + sleep 1 +fi + +if lsof -i :$TEMP_PORT > /dev/null 2>&1; then + echo "WARNING: Port $TEMP_PORT still in use, proceeding anyway..." else echo "Port $TEMP_PORT released successfully" fi diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash b/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash index a1831f1e8e76..4affbb553b0e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash +++ b/dev-packages/e2e-tests/test-applications/nuxt-5/nuxt-start-dev-server.bash @@ -25,8 +25,11 @@ if [ ! -f ".nuxt/dev/sentry.server.config.mjs" ]; then fi # 3. Cleanup +# `pkill -P` only kills direct children, so the grandchild dev server holding the +# port survives; newer Nuxt's directory-scoped dev lock then blocks the real start. echo "Found .nuxt/dev/sentry.server.config.mjs, stopping 'nuxt dev' process..." -pkill -P $DEV_PID || kill $DEV_PID +pkill -P $DEV_PID 2>/dev/null +kill $DEV_PID 2>/dev/null # Wait for port to be released echo "Waiting for port $TEMP_PORT to be released..." @@ -38,7 +41,13 @@ while lsof -i :$TEMP_PORT > /dev/null 2>&1 && [ $COUNTER -lt 10 ]; do done if lsof -i :$TEMP_PORT > /dev/null 2>&1; then - echo "WARNING: Port $TEMP_PORT still in use after 10 seconds, proceeding anyway..." + echo "Port $TEMP_PORT still in use, killing remaining processes bound to it..." + lsof -t -i :$TEMP_PORT | xargs -r kill -9 2>/dev/null + sleep 1 +fi + +if lsof -i :$TEMP_PORT > /dev/null 2>&1; then + echo "WARNING: Port $TEMP_PORT still in use, proceeding anyway..." else echo "Port $TEMP_PORT released successfully" fi diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index cb1be728f26f..b0c67a006185 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -19,7 +19,6 @@ import { getClient, getCurrentScope, getDynamicSamplingContextFromSpan, - getIsolationScope, getLocationHref, GLOBAL_OBJ, hasSpansEnabled, @@ -554,12 +553,6 @@ export const browserTracingIntegration = ((options: Partial { setCurrentClient(client); client.init(); - const oldIsolationScopePropCtx = getIsolationScope().getPropagationContext(); const oldCurrentScopePropCtx = getCurrentScope().getPropagationContext(); startBrowserTracingNavigationSpan(client, { name: 'test navigation span' }); - const newIsolationScopePropCtx = getIsolationScope().getPropagationContext(); const newCurrentScopePropCtx = getCurrentScope().getPropagationContext(); expect(oldCurrentScopePropCtx).toEqual({ @@ -950,24 +948,13 @@ describe('browserTracingIntegration', () => { propagationSpanId: expect.stringMatching(/[a-f0-9]{16}/), sampleRand: expect.any(Number), }); - expect(oldIsolationScopePropCtx).toEqual({ - traceId: expect.stringMatching(/[a-f0-9]{32}/), - sampleRand: expect.any(Number), - }); + expect(newCurrentScopePropCtx).toEqual({ traceId: expect.stringMatching(/[a-f0-9]{32}/), propagationSpanId: expect.stringMatching(/[a-f0-9]{16}/), sampleRand: expect.any(Number), }); - expect(newIsolationScopePropCtx).toEqual({ - traceId: expect.stringMatching(/[a-f0-9]{32}/), - propagationSpanId: expect.stringMatching(/[a-f0-9]{16}/), - sampleRand: expect.any(Number), - }); - - expect(newIsolationScopePropCtx.traceId).not.toEqual(oldIsolationScopePropCtx.traceId); expect(newCurrentScopePropCtx.traceId).not.toEqual(oldCurrentScopePropCtx.traceId); - expect(newIsolationScopePropCtx.propagationSpanId).not.toEqual(oldIsolationScopePropCtx.propagationSpanId); }); it("saves the span's positive sampling decision and its DSC on the propagationContext when the span finishes", () => { diff --git a/packages/cloudflare/.oxlintrc.json b/packages/cloudflare/.oxlintrc.json index 05cf6fc19fc6..ef16f327fffe 100644 --- a/packages/cloudflare/.oxlintrc.json +++ b/packages/cloudflare/.oxlintrc.json @@ -14,38 +14,7 @@ { "files": ["**/src/**"], "rules": { - "sdk/no-class-field-initializers": "off", - "no-restricted-imports": [ - "error", - { - "paths": [ - { - "name": "@sentry/node", - "message": "Do not import from `@sentry/node` in the Cloudflare SDK. It relies on Node.js APIs that are only available when the `nodejs_compat` flag is set. The only allowed importers are files in `src/nodejs_compat/`, which are exposed via the `@sentry/cloudflare/nodejs_compat/*` entry points." - }, - { - "name": "@sentry/server-utils", - "message": "Do not import from `@sentry/server-utils` in the Cloudflare SDK. It relies on Node.js APIs that are only available when the `nodejs_compat` flag is set. The only allowed importers are files in `src/nodejs_compat/`, which are exposed via the `@sentry/cloudflare/nodejs_compat/*` entry points." - } - ], - "patterns": [ - { - "group": ["@sentry/node/**"], - "message": "Do not import from `@sentry/node` in the Cloudflare SDK. It relies on Node.js APIs that are only available when the `nodejs_compat` flag is set. The only allowed importers are files in `src/nodejs_compat/`, which are exposed via the `@sentry/cloudflare/nodejs_compat/*` entry points." - }, - { - "group": ["@sentry/server-utils/**", "!@sentry/server-utils/no-diagnostic-channels"], - "message": "Do not import from `@sentry/server-utils` in the Cloudflare SDK. It relies on Node.js APIs that are only available when the `nodejs_compat` flag is set. The only allowed importers are files in `src/nodejs_compat/`, which are exposed via the `@sentry/cloudflare/nodejs_compat/*` entry points." - } - ] - } - ] - } - }, - { - "files": ["**/src/nodejs_compat/**", "**/src/vite/**"], - "rules": { - "no-restricted-imports": "off" + "sdk/no-class-field-initializers": "off" } } ] diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 0b0f8c60f224..78676502c59a 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -37,11 +37,6 @@ "import": "./build/esm/prod/request.js", "require": "./build/cjs/prod/request.js" }, - "./nodejs_compat": { - "types": "./build/types/nodejs_compat/index.d.ts", - "import": "./build/esm/prod/nodejs_compat/index.js", - "require": "./build/cjs/prod/nodejs_compat/index.js" - }, "./vite": { "types": "./build/types/vite/index.d.ts", "import": "./build/esm/prod/vite/index.js", diff --git a/packages/cloudflare/rollup.npm.config.mjs b/packages/cloudflare/rollup.npm.config.mjs index 839ccaa4c8db..908ad715e91c 100644 --- a/packages/cloudflare/rollup.npm.config.mjs +++ b/packages/cloudflare/rollup.npm.config.mjs @@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.ts', 'src/nodejs_compat/index.ts', 'src/vite/index.ts'], + entrypoints: ['src/index.ts', 'src/request.ts', 'src/vite/index.ts'], }), { splitDevProd: true }, ); diff --git a/packages/cloudflare/src/baseSdk.ts b/packages/cloudflare/src/baseSdk.ts new file mode 100644 index 000000000000..2e6ff095b9d5 --- /dev/null +++ b/packages/cloudflare/src/baseSdk.ts @@ -0,0 +1,134 @@ +import type { Integration } from '@sentry/core'; +import { + consoleIntegration, + conversationIdIntegration, + dedupeIntegration, + functionToStringIntegration, + getIntegrationsToSetup, + GLOBAL_OBJ, + inboundFiltersIntegration, + initAndBind, + linkedErrorsIntegration, + requestDataIntegration, + stackParserFromStackParserOptions, +} from '@sentry/core'; +import type { CloudflareClientOptions, CloudflareOptions } from './client'; +import { CloudflareClient } from './client'; +import { makeFlushLock } from './flush'; +import { fetchIntegration } from './integrations/fetch'; +import { httpServerIntegration } from './integrations/httpServer'; +import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from './integrations/spotlight'; +import { setupOpenTelemetryTracer } from './opentelemetry/tracer'; +import { makeCloudflareTransport } from './transport'; +import { defaultStackParser } from './vendor/stacktrace'; + +/** + * Instantiate the channel-subscriber factories the `@sentry/cloudflare/vite` + * plugin registered on the global marker. The plugin splices a small snippet + * into each instrumented module that `.set`s its factory here (keyed by export + * name), so the marker holds one factory per package actually bundled. + * + * The marker is read directly instead of importing the factories, so a worker + * built without the plugin — where the channels never fire — ships none of this + * code. + * TODO(v11): Use `@sentry/server-utils/orchestrion` once we move to `nodejs_compat` by default. + */ +function getRegisteredChannelIntegrations(): Integration[] { + const registered = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations; + + return registered ? [...registered.values()].map(factory => factory()) : []; +} + +/** + * Get the default integrations that run on any Workers-compatible runtime, i.e. without the + * `nodejs_compat` compatibility flag. + * + * `getDefaultIntegrations` in `sdk.ts` extends this set with the integrations that do depend on + * Node.js APIs. Keeping the two apart is what allows `wrapRequestHandler` to stay usable on runtimes + * that cannot enable `nodejs_compat`, such as Shopify Oxygen. + */ +export function getBaseDefaultIntegrations(options: CloudflareOptions): Integration[] { + return [ + // The Dedupe integration should not be used in workflows because we want to + // capture all step failures, even if they are the same error. + ...(options.enableDedupe === false ? [] : [dedupeIntegration()]), + // TODO(v11): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` + // eslint-disable-next-line typescript/no-deprecated + inboundFiltersIntegration(), + functionToStringIntegration(), + conversationIdIntegration(), + linkedErrorsIntegration(), + fetchIntegration(), + httpServerIntegration(), + // oxlint-disable-next-line typescript/no-deprecated + requestDataIntegration(), + consoleIntegration(), + // The orchestrion diagnostics-channel subscribers (mysql, pg, …). The + // `@sentry/cloudflare/vite` plugin injects the channels at build time and, + // next to each, a snippet that registers the matching subscriber factory on + // the global marker. Read from there instead of importing them so bundles + // built without the plugin — where the channels would never fire — don't + // ship the code. + ...getRegisteredChannelIntegrations(), + ]; +} + +/** + * Initializes the Cloudflare SDK with the passed default integrations. + * + * The default integrations are injected rather than imported so that this module stays free of + * Node.js-only code. `request.ts` — which backs both `wrapRequestHandler` and the + * `@sentry/cloudflare/request` entry point, and therefore has to work on runtimes without the + * `nodejs_compat` compatibility flag — creates its client from here instead of from `sdk.ts`. + */ +export function initWithDefaultIntegrations( + options: CloudflareOptions, + getDefaultIntegrationsImpl: (options: CloudflareOptions) => Integration[], +): CloudflareClient | undefined { + if (options.defaultIntegrations === undefined) { + options.defaultIntegrations = getDefaultIntegrationsImpl(options); + } + + const flushLock = options.ctx ? makeFlushLock(options.ctx) : undefined; + delete options.ctx; + + const clientOptions: CloudflareClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeCloudflareTransport, + flushLock, + }; + + /*! rollup-include-development-only */ + if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { + clientOptions.integrations.push( + spotlightIntegration({ + sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, + }), + ); + } + /*! rollup-include-development-only-end */ + + /** + * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility + * via a custom trace provider. + * This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry. + * HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope. + * This should be good enough for many, but not all integrations. + */ + if (!options.skipOpenTelemetrySetup) { + setupOpenTelemetryTracer(); + } + + return initAndBind(CloudflareClient, clientOptions) as CloudflareClient; +} + +/** + * Initializes the Cloudflare SDK with only the default integrations from + * {@link getBaseDefaultIntegrations}, i.e. those that work without the `nodejs_compat` + * compatibility flag. + */ +export function initBaseSdk(options: CloudflareOptions): CloudflareClient | undefined { + return initWithDefaultIntegrations(options, getBaseDefaultIntegrations); +} diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index ff4ae839f0a0..e087ef8eb3bc 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -6,7 +6,8 @@ import type { CloudflareOptions } from './client'; import { ensureInstrumented } from './instrument'; import { instrumentEnv } from './instrumentations/worker/instrumentEnv'; import { getFinalOptions } from './options'; -import { wrapRequestHandler } from './request'; +import { wrapRequestHandlerWithInit } from './request'; +import { init } from './sdk'; import { instrumentContext } from './utils/instrumentContext'; import { extractRpcMeta } from './utils/rpcMeta'; import { getEffectiveRpcPropagation } from './utils/rpcOptions'; @@ -89,9 +90,13 @@ function instrumentDurableObjectHandlers>( original => new Proxy(original, { apply(target, thisArg, args) { - return wrapRequestHandler({ options, request: args[0], context }, () => { - return Reflect.apply(target, thisArg, args); - }); + return wrapRequestHandlerWithInit( + { options, request: args[0], context }, + () => { + return Reflect.apply(target, thisArg, args); + }, + init, + ); }, }), ); diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 20124b3b8554..d107b6a29555 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -128,6 +128,7 @@ export { httpServerIntegration } from './integrations/httpServer'; export { fetchIntegration } from './integrations/fetch'; export { spotlightIntegration } from './integrations/spotlight'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; +export { prismaIntegration } from '@sentry/server-utils'; // eslint-disable-next-line typescript/no-deprecated export { instrumentD1WithSentry } from './instrumentations/worker/instrumentD1'; diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts b/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts index 8a27edc32073..4f96447e9e34 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentFetch.ts @@ -3,7 +3,8 @@ import type { env as cloudflareEnv, WorkerEntrypoint } from 'cloudflare:workers' import type { CloudflareOptions } from '../../client'; import { ensureInstrumented } from '../../instrument'; import { getFinalOptions } from '../../options'; -import { wrapRequestHandler } from '../../request'; +import { wrapRequestHandlerWithInit } from '../../request'; +import { init } from '../../sdk'; import { instrumentContext } from '../../utils/instrumentContext'; import { instrumentEnv } from './instrumentEnv'; @@ -35,7 +36,7 @@ export function instrumentExportedHandlerFetch target.apply(thisArg, args)); + return wrapRequestHandlerWithInit({ options, request, context }, () => target.apply(thisArg, args), init); }, }), ); @@ -62,7 +63,11 @@ export function instrumentWorkerEntrypointFetch( return Reflect.apply(target, thisArg, args); } - return wrapRequestHandler({ options, request, context }, () => Reflect.apply(target, thisArg, args)); + return wrapRequestHandlerWithInit( + { options, request, context }, + () => Reflect.apply(target, thisArg, args), + init, + ); }, }); } diff --git a/packages/cloudflare/src/integrations/tracing/vercelai.ts b/packages/cloudflare/src/integrations/tracing/vercelai.ts index 9d28d41d5bb0..242d476c342f 100644 --- a/packages/cloudflare/src/integrations/tracing/vercelai.ts +++ b/packages/cloudflare/src/integrations/tracing/vercelai.ts @@ -9,31 +9,18 @@ */ import type { IntegrationFn } from '@sentry/core'; -import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; - -const INTEGRATION_NAME = 'VercelAI'; - -interface VercelAiOptions { - /** - * Enable or disable truncation of recorded input messages. - * Defaults to `true`. - */ - enableTruncation?: boolean; - - // `recordInputs`/`recordOutputs` are intentionally omitted: this entrypoint only post-processes - // spans the AI SDK already emitted, so it cannot decide whether inputs/outputs are recorded. - // Control this per call via `experimental_telemetry.recordInputs`/`recordOutputs`, or use the - // `@sentry/cloudflare/nodejs_compat` entrypoint for integration-level control on ai >= 7. -} +import { addVercelAiProcessors, defineIntegration, extendIntegration } from '@sentry/core'; +import { vercelAiIntegration, type VercelAiOptions } from '@sentry/server-utils'; const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { - return { - name: INTEGRATION_NAME, + const inner = vercelAiIntegration(options); + + return extendIntegration(inner, { options, setup(client) { addVercelAiProcessors(client); }, - }; + }); }) satisfies IntegrationFn; /** diff --git a/packages/cloudflare/src/nodejs_compat/index.ts b/packages/cloudflare/src/nodejs_compat/index.ts deleted file mode 100644 index 3c259e10e7e9..000000000000 --- a/packages/cloudflare/src/nodejs_compat/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from '../index'; -export { prismaIntegration } from '@sentry/server-utils'; -export { vercelAIIntegration } from './integrations/tracing/vercelai'; diff --git a/packages/cloudflare/src/nodejs_compat/integrations/tracing/vercelai.ts b/packages/cloudflare/src/nodejs_compat/integrations/tracing/vercelai.ts deleted file mode 100644 index e808b3c04443..000000000000 --- a/packages/cloudflare/src/nodejs_compat/integrations/tracing/vercelai.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * This is a copy of the Vercel AI integration from the Cloudflare SDK (src/integrations/tracing/vercelai.ts). - * - * The only difference is that it includes tracing channel support - * because the default Vercel AI integration needs `nodejs_als`, while this one uses `nodejs_compat`. - */ - -import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { vercelAiIntegration as serverUtilsVercelAiIntegration, type VercelAiOptions } from '@sentry/server-utils'; -import { vercelAIIntegration as cloudflareVercelAIIntegration } from '../../../integrations/tracing/vercelai'; - -const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { - const inner = serverUtilsVercelAiIntegration(options); - const instrumentation = cloudflareVercelAIIntegration(options); - - return { - ...inner, - ...instrumentation, - }; -}) satisfies IntegrationFn; - -/** - * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library. - * This integration is not enabled by default, you need to manually add it. - * - * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry). - * - * You need to enable collecting spans for a specific call by setting - * `experimental_telemetry.isEnabled` to `true` in the first argument of the function call. - * - * ```javascript - * const result = await generateText({ - * model: openai('gpt-4-turbo'), - * experimental_telemetry: { isEnabled: true }, - * }); - * ``` - * - * If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each - * function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs` - * to `true`. - * - * ```javascript - * const result = await generateText({ - * model: openai('gpt-4-turbo'), - * experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true }, - * }); - */ -export const vercelAIIntegration = defineIntegration(_vercelAIIntegration); diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts index 2f9c966cb53b..518b1c448017 100644 --- a/packages/cloudflare/src/pages-plugin.ts +++ b/packages/cloudflare/src/pages-plugin.ts @@ -1,7 +1,8 @@ import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; import type { CloudflareOptions } from './client'; import type { ExecutionContextCompat } from './executionContext'; -import { wrapRequestHandler } from './request'; +import { wrapRequestHandlerWithInit } from './request'; +import { init } from './sdk'; /** * Plugin middleware for Cloudflare Pages. @@ -57,6 +58,10 @@ export function sentryPagesPlugin< // A Pages `EventPluginContext` is not a Workers `ExecutionContext`, but `wrapRequestHandler` only // uses `waitUntil` and a `'storage' in context` check, both of which this satisfies. const executionContext = { ...context, props: {} } as unknown as ExecutionContextCompat; - return wrapRequestHandler({ options, request: context.request, context: executionContext }, () => context.next()); + return wrapRequestHandlerWithInit( + { options, request: context.request, context: executionContext }, + () => context.next(), + init, + ); }; } diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index 2adf4cbf2c24..e97d21c5c916 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -13,11 +13,11 @@ import { withIsolationScope, } from '@sentry/core'; import { captureIncomingRequestBody } from './integrations/httpServer'; -import type { CloudflareOptions } from './client'; +import { initBaseSdk } from './baseSdk'; +import type { CloudflareClient, CloudflareOptions } from './client'; import type { ExecutionContextCompat } from './executionContext'; import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; -import { init } from './sdk'; import { classifyResponseStreaming } from './utils/streaming'; function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string { @@ -42,12 +42,35 @@ interface RequestHandlerWrapperOptions { captureErrors?: boolean; } +type InitSdk = (options: CloudflareOptions) => CloudflareClient | undefined; + /** - * Wraps a cloudflare request handler in Sentry instrumentation + * Wraps a cloudflare request handler in Sentry instrumentation. + * + * The client is set up with the default integrations that work without the `nodejs_compat` + * compatibility flag, so that this also works on runtimes that cannot enable it (e.g. Shopify + * Oxygen). On a runtime that has `nodejs_compat`, pass `defaultIntegrations: + * getDefaultIntegrations(options)` in `options` to get the full set instead. */ export function wrapRequestHandler( wrapperOptions: RequestHandlerWrapperOptions, handler: (...args: unknown[]) => Response | Promise, +): Promise { + return wrapRequestHandlerWithInit(wrapperOptions, handler, initBaseSdk); +} + +/** + * Same as {@link wrapRequestHandler}, but with the SDK initialization injected. + * + * Wrappers that are only reachable from the main entry point — where `nodejs_compat` is a + * requirement anyway — pass `init` from `sdk.ts` to get the full default integrations. + * + * @internal + */ +export function wrapRequestHandlerWithInit( + wrapperOptions: RequestHandlerWrapperOptions, + handler: (...args: unknown[]) => Response | Promise, + initSdk: InitSdk, ): Promise { return withIsolationScope(async isolationScope => { const { options, request, captureErrors = true } = wrapperOptions; @@ -61,7 +84,7 @@ export function wrapRequestHandler( const waitUntil = context ? getOriginalWaitUntil(context)?.bind(context) : undefined; const errorMechanismType = getRequestErrorMechanismType(context); - const client = init({ ...options, ctx: context }); + const client = initSdk({ ...options, ctx: context }); isolationScope.setClient(client); const urlObject = parseStringToURLObject(request.url); diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index 87ee09e12176..f96cbdc0626f 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -1,68 +1,20 @@ import type { Integration } from '@sentry/core'; -import { - consoleIntegration, - conversationIdIntegration, - dedupeIntegration, - functionToStringIntegration, - getIntegrationsToSetup, - GLOBAL_OBJ, - inboundFiltersIntegration, - initAndBind, - linkedErrorsIntegration, - requestDataIntegration, - stackParserFromStackParserOptions, -} from '@sentry/core'; -import type { CloudflareClientOptions, CloudflareOptions } from './client'; -import { CloudflareClient } from './client'; -import { makeFlushLock } from './flush'; -import { httpServerIntegration } from './integrations/httpServer'; -import { fetchIntegration } from './integrations/fetch'; -import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from './integrations/spotlight'; -import { setupOpenTelemetryTracer } from './opentelemetry/tracer'; -import { makeCloudflareTransport } from './transport'; -import { defaultStackParser } from './vendor/stacktrace'; +import { vercelAIIntegration } from './integrations/tracing/vercelai'; +import { getBaseDefaultIntegrations, initWithDefaultIntegrations } from './baseSdk'; +import type { CloudflareClient, CloudflareOptions } from './client'; /** - * Instantiate the channel-subscriber factories the `@sentry/cloudflare/vite` - * plugin registered on the global marker. The plugin splices a small snippet - * into each instrumented module that `.set`s its factory here (keyed by export - * name), so the marker holds one factory per package actually bundled. + * Get the default integrations for the Cloudflare SDK. * - * The marker is read directly instead of importing the factories, so a worker - * built without the plugin — where the channels never fire — ships none of this - * code. - * TODO(v11): Use `@sentry/server-utils/orchestrion` once we move to `nodejs_compat` by default. + * This is the full set and requires the `nodejs_compat` compatibility flag. Runtimes that cannot + * enable it (e.g. Shopify Oxygen) go through `wrapRequestHandler`, which only sets up + * `getBaseDefaultIntegrations`. */ -function getRegisteredChannelIntegrations(): Integration[] { - const registered = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.integrations; - - return registered ? [...registered.values()].map(factory => factory()) : []; -} - -/** Get the default integrations for the Cloudflare SDK. */ export function getDefaultIntegrations(options: CloudflareOptions): Integration[] { return [ - // The Dedupe integration should not be used in workflows because we want to - // capture all step failures, even if they are the same error. - ...(options.enableDedupe === false ? [] : [dedupeIntegration()]), - // TODO(v11): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` - // eslint-disable-next-line typescript/no-deprecated - inboundFiltersIntegration(), - functionToStringIntegration(), - conversationIdIntegration(), - linkedErrorsIntegration(), - fetchIntegration(), - httpServerIntegration(), - // oxlint-disable-next-line typescript/no-deprecated - requestDataIntegration(), - consoleIntegration(), - // The orchestrion diagnostics-channel subscribers (mysql, pg, …). The - // `@sentry/cloudflare/vite` plugin injects the channels at build time and, - // next to each, a snippet that registers the matching subscriber factory on - // the global marker. Read from there instead of importing them so bundles - // built without the plugin — where the channels would never fire — don't - // ship the code. - ...getRegisteredChannelIntegrations(), + ...getBaseDefaultIntegrations(options), + // Subscribes to the `ai` SDK's native `node:diagnostics_channel` telemetry channel. + vercelAIIntegration(), ]; } @@ -70,41 +22,5 @@ export function getDefaultIntegrations(options: CloudflareOptions): Integration[ * Initializes the cloudflare SDK. */ export function init(options: CloudflareOptions): CloudflareClient | undefined { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - - const flushLock = options.ctx ? makeFlushLock(options.ctx) : undefined; - delete options.ctx; - - const clientOptions: CloudflareClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeCloudflareTransport, - flushLock, - }; - - /*! rollup-include-development-only */ - if (options.spotlight && !clientOptions.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { - clientOptions.integrations.push( - spotlightIntegration({ - sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, - }), - ); - } - /*! rollup-include-development-only-end */ - - /** - * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility - * via a custom trace provider. - * This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry. - * HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope. - * This should be good enough for many, but not all integrations. - */ - if (!options.skipOpenTelemetrySetup) { - setupOpenTelemetryTracer(); - } - - return initAndBind(CloudflareClient, clientOptions) as CloudflareClient; + return initWithDefaultIntegrations(options, getDefaultIntegrations); } diff --git a/packages/cloudflare/test/requestModuleGraph.test.ts b/packages/cloudflare/test/requestModuleGraph.test.ts new file mode 100644 index 000000000000..2100f4a47311 --- /dev/null +++ b/packages/cloudflare/test/requestModuleGraph.test.ts @@ -0,0 +1,82 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +/** + * `wrapRequestHandler` — exposed both from the main entry point and from `@sentry/cloudflare/request` + * — has to run on runtimes that cannot enable the `nodejs_compat` compatibility flag, such as Shopify + * Oxygen. A single `node:` import anywhere in its module graph breaks those workers at startup with + * `No such module "node:…"`. + * + * Bundlers usually tree-shake an unused Node.js import away, but that is not something to rely on, so + * assert on the module graph itself rather than on any bundler's output. + */ +const SRC_DIR = resolve(__dirname, '../src'); +const ENTRY_POINT = join(SRC_DIR, 'request.ts'); + +const IMPORT_SPECIFIER_REGEX = /\bfrom\s*['"]([^'"]+)['"]|\bimport\s*['"]([^'"]+)['"]/g; + +function resolveRelativeImport(importer: string, specifier: string): string { + const withoutExtension = join(dirname(importer), specifier); + const candidates = [`${withoutExtension}.ts`, join(withoutExtension, 'index.ts')]; + const resolved = candidates.find(candidate => existsSync(candidate)); + + if (!resolved) { + throw new Error(`Could not resolve '${specifier}' imported from '${relative(SRC_DIR, importer)}'`); + } + + return resolved; +} + +/** Walks the module graph of `entryPoint`, returning the local modules and the external specifiers. */ +function collectModuleGraph(entryPoint: string): { modules: string[]; externals: string[] } { + const modules = new Set(); + const externals = new Set(); + const queue = [entryPoint]; + + while (queue.length) { + const importer = queue.pop() as string; + + if (modules.has(importer)) { + continue; + } + modules.add(importer); + + for (const match of readFileSync(importer, 'utf8').matchAll(IMPORT_SPECIFIER_REGEX)) { + const specifier = match[1] ?? (match[2] as string); + + if (specifier.startsWith('.')) { + queue.push(resolveRelativeImport(importer, specifier)); + } else { + externals.add(specifier); + } + } + } + + return { + modules: [...modules].map(module => relative(SRC_DIR, module)).sort(), + externals: [...externals].sort(), + }; +} + +describe('module graph of `wrapRequestHandler`', () => { + const { modules, externals } = collectModuleGraph(ENTRY_POINT); + + it('contains no Node.js built-in module', () => { + expect(externals.filter(specifier => specifier.startsWith('node:'))).toEqual([]); + }); + + it('contains no package that depends on Node.js APIs', () => { + // `@sentry/server-utils` subscribes to `node:diagnostics_channel` and `@sentry/node` needs Node.js + // throughout. Both are only allowed in `sdk.ts`, `index.ts` and `vite/` (see `.oxlintrc.json`). + expect(externals.filter(specifier => /^@sentry\/(node|server-utils)(\/|$)/.test(specifier))).toEqual([]); + }); + + it('does not reach `async.ts`, the only shipped module importing `node:async_hooks`', () => { + expect(modules).not.toContain('async.ts'); + }); + + it('does not reach `sdk.ts`, which holds the default integrations that need `nodejs_compat`', () => { + expect(modules).not.toContain('sdk.ts'); + }); +}); diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index e2322766287b..12623aa224d9 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,10 +1,8 @@ import type { AttributeObject, RawAttribute, RawAttributes } from './attributes'; -import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; +import { getClient, getCurrentScope, getIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { CaptureContext } from './scope'; import { closeSession, makeSession, updateSession } from './session'; -import { startNewTrace } from './tracing/trace'; -import type { CheckIn, FinishedCheckIn, MonitorConfig } from './types/checkin'; import type { Event, EventHint } from './types/event'; import type { EventProcessor } from './types/eventprocessor'; import type { Extra, Extras } from './types/extra'; @@ -13,12 +11,9 @@ import type { Session, SessionContext } from './types/session'; import type { SeverityLevel } from './types/severity'; import type { User } from './types/user'; import { debug } from './utils/debug-logger'; -import { isThenable } from './utils/is'; -import { uuid4 } from './utils/misc'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; import { getCombinedScopeData } from './utils/scopeData'; -import { timestampInSeconds } from './utils/time'; import { GLOBAL_OBJ } from './utils/worldwide'; /** @@ -182,76 +177,6 @@ export function lastEventId(): string | undefined { return getIsolationScope().lastEventId(); } -/** - * Create a cron monitor check in and send it to Sentry. - * - * @param checkIn An object that describes a check in. - * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want - * to create a monitor automatically when sending a check in. - */ -export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string { - const scope = getCurrentScope(); - const client = getClient(); - if (!client) { - DEBUG_BUILD && debug.warn('Cannot capture check-in. No client defined.'); - } else if (!client.captureCheckIn) { - DEBUG_BUILD && debug.warn('Cannot capture check-in. Client does not support sending check-ins.'); - } else { - return client.captureCheckIn(checkIn, upsertMonitorConfig, scope); - } - - return uuid4(); -} - -/** - * Wraps a callback with a cron monitor check in. The check in will be sent to Sentry when the callback finishes. - * - * @param monitorSlug The distinct slug of the monitor. - * @param callback Callback to be monitored - * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want - * to create a monitor automatically when sending a check in. - */ -export function withMonitor( - monitorSlug: CheckIn['monitorSlug'], - callback: () => T, - upsertMonitorConfig?: MonitorConfig, -): T { - function runCallback(): T { - const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, upsertMonitorConfig); - const now = timestampInSeconds(); - - function finishCheckIn(status: FinishedCheckIn['status']): void { - captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - now }); - } - // Default behavior without isolateTrace - let maybePromiseResult: T; - try { - maybePromiseResult = callback(); - } catch (e) { - finishCheckIn('error'); - throw e; - } - - if (isThenable(maybePromiseResult)) { - return maybePromiseResult.then( - r => { - finishCheckIn('ok'); - return r; - }, - e => { - finishCheckIn('error'); - throw e; - }, - ) as T; - } - finishCheckIn('ok'); - - return maybePromiseResult; - } - - return withIsolationScope(() => (upsertMonitorConfig?.isolateTrace ? startNewTrace(runCallback) : runCallback())); -} - /** * Call `flush()` on the current client, if there is one. See {@link Client.flush}. * diff --git a/packages/core/src/monitor.ts b/packages/core/src/monitor.ts new file mode 100644 index 000000000000..4d99f7fd5e8f --- /dev/null +++ b/packages/core/src/monitor.ts @@ -0,0 +1,98 @@ +import { getClient, getCurrentScope, withIsolationScope } from './currentScopes'; +import { DEBUG_BUILD } from './debug-build'; +import { startNewTrace } from './tracing/trace'; +import type { CheckIn, FinishedCheckIn, MonitorConfig } from './types/checkin'; +import { debug } from './utils/debug-logger'; +import { isThenable } from './utils/is'; +import { uuid4 } from './utils/misc'; +import { timestampInSeconds } from './utils/time'; +import { isContinuingTrace } from './utils/tracing'; + +/** + * Wraps a callback with a cron monitor check in. The check in will be sent to Sentry when the callback finishes. + * + * @param monitorSlug The distinct slug of the monitor. + * @param callback Callback to be monitored + * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want + * to create a monitor automatically when sending a check in. + */ +export function withMonitor( + monitorSlug: CheckIn['monitorSlug'], + callback: () => T, + upsertMonitorConfig?: MonitorConfig, +): T { + function runCallback(): T { + const checkInId = captureCheckIn({ monitorSlug, status: 'in_progress' }, upsertMonitorConfig); + const now = timestampInSeconds(); + + function finishCheckIn(status: FinishedCheckIn['status']): void { + captureCheckIn({ monitorSlug, status, checkInId, duration: timestampInSeconds() - now }); + } + // Default behavior without isolateTrace + let maybePromiseResult: T; + try { + maybePromiseResult = callback(); + } catch (e) { + finishCheckIn('error'); + throw e; + } + + if (isThenable(maybePromiseResult)) { + return maybePromiseResult.then( + r => { + finishCheckIn('ok'); + return r; + }, + e => { + finishCheckIn('error'); + throw e; + }, + ) as T; + } + finishCheckIn('ok'); + + return maybePromiseResult; + } + + // `withIsolationScope` gives the fork its own trace, so unless `isolateTrace` is set we restore the + // parent's trace below. Copied rather than aliased: sharing the object with the parent scope would + // let in-place writes inside the callback (e.g. the HTTP server integration assigning + // `propagationSpanId`) rewrite the parent's trace. + const oldPropagationContext = { ...getCurrentScope().getPropagationContext() }; + + return withIsolationScope(() => { + if (upsertMonitorConfig?.isolateTrace) { + return startNewTrace(runCallback); + } + + // Mirrors the reset condition in the async context strategies: only a fork that was given a fresh + // trace needs the parent's trace put back. + const scope = getCurrentScope(); + if (!isContinuingTrace(scope.getPropagationContext())) { + scope.setPropagationContext(oldPropagationContext); + } + + return runCallback(); + }); +} + +/** + * Create a cron monitor check in and send it to Sentry. + * + * @param checkIn An object that describes a check in. + * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want + * to create a monitor automatically when sending a check in. + */ +export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string { + const scope = getCurrentScope(); + const client = getClient(); + if (!client) { + DEBUG_BUILD && debug.warn('Cannot capture check-in. No client defined.'); + } else if (!client.captureCheckIn) { + DEBUG_BUILD && debug.warn('Cannot capture check-in. Client does not support sending check-ins.'); + } else { + return client.captureCheckIn(checkIn, upsertMonitorConfig, scope); + } + + return uuid4(); +} diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index 2d75b93b311e..03dc9635ff11 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -12,8 +12,6 @@ export * from './tracing'; export * from './semanticAttributes'; export { createEventEnvelope, createSessionEnvelope } from './envelope'; export { - captureCheckIn, - withMonitor, captureException, captureEvent, captureMessage, @@ -36,6 +34,7 @@ export { captureSession, addEventProcessor, } from './exports'; +export { withMonitor, captureCheckIn } from './monitor'; export { getCurrentScope, getIsolationScope, @@ -277,6 +276,7 @@ export { TRACEPARENT_REGEXP, extractTraceparentData, generateSentryTraceHeader, + isContinuingTrace, propagationContextFromHeaders, shouldContinueTrace, generateTraceparentHeader, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 115f8097b190..e8528cbef2d1 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -367,7 +367,7 @@ function createChildOrRootSpan({ const isolationScope = getIsolationScope(); if (!hasSpansEnabled()) { - const scopePropagationContext = { ...isolationScope.getPropagationContext(), ...scope.getPropagationContext() }; + const scopePropagationContext = scope.getPropagationContext(); const traceId = parentSpan ? parentSpan.spanContext().traceId : scopePropagationContext.traceId; // The placeholder is a thin marker; it carries no sampling decision or DSC. Both are read from @@ -432,15 +432,7 @@ function createChildOrRootSpan({ freezeDscOnSpan(span, dsc); } else { - const { - traceId, - dsc, - parentSpanId, - sampled: parentSampled, - } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; + const { traceId, dsc, parentSpanId, sampled: parentSampled } = scope.getPropagationContext(); span = _startRootSpan( { diff --git a/packages/core/src/utils/tracing.ts b/packages/core/src/utils/tracing.ts index 85b660c2b8b0..b121fa13ce85 100644 --- a/packages/core/src/utils/tracing.ts +++ b/packages/core/src/utils/tracing.ts @@ -52,6 +52,17 @@ export function extractTraceparentData(traceparent?: string): TraceparentData | }; } +/** + * Whether a propagation context continues an incoming trace, rather than being the head of a new one. + * + * Both fields have to be checked. `parentSpanId` is unset when the incoming `sentry-trace` header + * carried no span id, which the header format allows. `dsc` is unset when a remote parent arrived + * without any incoming baggage. Either one on its own therefore misses a continued trace. + */ +export function isContinuingTrace(propagationContext: PropagationContext): boolean { + return !!propagationContext.parentSpanId || !!propagationContext.dsc; +} + /** * Create a propagation context from incoming headers or * creates a minimal new one if the headers are undefined. diff --git a/packages/core/test/lib/monitor.test.ts b/packages/core/test/lib/monitor.test.ts new file mode 100644 index 000000000000..c2ce3000402c --- /dev/null +++ b/packages/core/test/lib/monitor.test.ts @@ -0,0 +1,60 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { getCurrentScope, getGlobalScope, getIsolationScope } from '../../src/currentScopes'; +import { withMonitor } from '../../src/monitor'; +import { setCurrentClient } from '../../src/sdk'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; + +describe('withMonitor', () => { + beforeEach(() => { + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + const client = new TestClient(getDefaultTestClientOptions({ dsn: 'https://username@domain/123' })); + setCurrentClient(client); + client.init(); + }); + + it('keeps the parent trace when not isolating the trace', () => { + const parentTraceId = getCurrentScope().getPropagationContext().traceId; + + withMonitor('cron-job', () => { + expect(getCurrentScope().getPropagationContext().traceId).toBe(parentTraceId); + }); + }); + + it('starts a separate trace when isolateTrace is set', () => { + const parentTraceId = getCurrentScope().getPropagationContext().traceId; + + withMonitor( + 'cron-job', + () => { + expect(getCurrentScope().getPropagationContext().traceId).not.toBe(parentTraceId); + }, + { schedule: { type: 'crontab', value: '* * * * *' }, isolateTrace: true }, + ); + }); + + // The parent's propagation context is restored onto the monitor scope, so it + // must be copied rather than aliased. Several call sites mutate the + // propagation context in place (e.g. the Node HTTP server integration + // assigns `propagationSpanId`), which would otherwise rewrite the parent's + // trace from inside the callback. + it('does not let the callback mutate the parent propagation context', () => { + const parentScope = getCurrentScope(); + const parentPropagationContext = parentScope.getPropagationContext(); + + withMonitor('cron-job', () => { + const propagationContext = getCurrentScope().getPropagationContext(); + + expect(propagationContext).not.toBe(parentPropagationContext); + expect(propagationContext).toEqual(parentPropagationContext); + + propagationContext.propagationSpanId = 'deadbeefdeadbeef'; + propagationContext.traceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + }); + + expect(parentScope.getPropagationContext()).toEqual(parentPropagationContext); + expect(parentPropagationContext.propagationSpanId).toBeUndefined(); + }); +}); diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 588585630621..8ca2f6cda39d 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, type Mock, test, vi } from 'vitest'; import type { Client } from '../../src/client'; import { getCurrentScope } from '../../src/currentScopes'; -import { captureCheckIn } from '../../src/exports'; +import { captureCheckIn } from '../../src/monitor'; import { installedIntegrations } from '../../src/integration'; import { initAndBind, setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types/integration'; diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 367bf36aeab8..83c91683d7f6 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1471,6 +1471,30 @@ describe('startInactiveSpan', () => { expect(getActiveSpan()).toBeUndefined(); }); + // A root span reads its trace from the current scope only. The isolation + // scope's propagation context must not contribute: mixing the two yields + // a span whose `parent_span_id` and frozen DSC describe a different trace + // than its own `trace_id`. + it('ignores the propagation context on the isolation scope', () => { + const isolationTraceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + getIsolationScope().setPropagationContext({ + traceId: isolationTraceId, + sampleRand: 0.1, + parentSpanId: 'bbbbbbbbbbbbbbbb', + sampled: true, + dsc: { trace_id: isolationTraceId, transaction: 'from-isolation-scope' }, + }); + + const currentTraceId = getCurrentScope().getPropagationContext().traceId; + const span = startInactiveSpan({ name: 'GET users/[id]' }); + + expect(spanToJSON(span).trace_id).toBe(currentTraceId); + expect(spanToJSON(span).parent_span_id).toBeUndefined(); + expect(getDynamicSamplingContextFromSpan(span)).toEqual( + expect.objectContaining({ trace_id: currentTraceId, transaction: 'GET users/[id]' }), + ); + }); + it('allows to pass a scope', () => { const initialScope = getCurrentScope(); diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 9bd61aece2cc..842c0456b802 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -22,156 +22,6 @@ export type SentryNuxtServerOptions = Parameters[0] & { enableNitroErrorHandler?: boolean; }; -type SourceMapsOptions = { - /** - * Suppresses all logs. - * - * @default false - * @deprecated Use option `silent` instead of `sourceMapsUploadOptions.silent` - */ - silent?: boolean; - - /** - * When an error occurs during release creation or sourcemaps upload, the plugin will call this function. - * - * By default, the plugin will simply throw an error, thereby stopping the bundling process. - * If an `errorHandler` callback is provided, compilation will continue, unless an error is - * thrown in the provided callback. - * - * To allow compilation to continue but still emit a warning, set this option to the following: - * - * ```js - * (err) => { - * console.warn(err); - * } - * ``` - * - * @deprecated Use option `errorHandler` instead of `sourceMapsUploadOptions.errorHandler` - */ - errorHandler?: (err: Error) => void; - - /** - * Options related to managing the Sentry releases for a build. - * - * More info: https://docs.sentry.io/product/releases/ - * - * @deprecated Use option `release` instead of `sourceMapsUploadOptions.release` - */ - release?: { - /** - * Unique identifier for the release you want to create. - * - * This value can also be specified via the `SENTRY_RELEASE` environment variable. - * - * Defaults to automatically detecting a value for your environment. - * This includes values for Cordova, Heroku, AWS CodeBuild, CircleCI, Xcode, and Gradle, and otherwise uses the git `HEAD`'s commit SHA. - * (the latter requires access to git CLI and for the root directory to be a valid repository) - * - * If you didn't provide a value and the plugin can't automatically detect one, no release will be created. - * - * @deprecated Use `release.name` instead of `sourceMapsUploadOptions.release.name` - */ - name?: string; - }; - - /** - * If this flag is `true`, and an auth token is detected, the Sentry SDK will - * automatically generate and upload source maps to Sentry during a production build. - * - * @default true - * @deprecated Use option `sourcemaps.disable` instead of `sourceMapsUploadOptions.enabled` - */ - enabled?: boolean; - - /** - * The auth token to use when uploading source maps to Sentry. - * - * Instead of specifying this option, you can also set the `SENTRY_AUTH_TOKEN` environment variable. - * - * To create an auth token, follow this guide: - * @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens - * @deprecated Use option `authToken` instead of `sourceMapsUploadOptions.authToken` - */ - authToken?: string; - - /** - * The organization slug of your Sentry organization. - * Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable. - * @deprecated Use option `org` instead of `sourceMapsUploadOptions.org` - */ - org?: string; - - /** - * The URL of your Sentry instance if you're using self-hosted Sentry. - * - * @default https://sentry.io by default the plugin will point towards the Sentry SaaS URL - * @deprecated Use `sentryUrl` instead of `sourceMapsUploadOptions.url` - */ - url?: string; - - /** - * The project slug of your Sentry project. - * Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable. - * - * @deprecated Use option `project` instead of `sourceMapsUploadOptions.project` - */ - project?: string; - - /** - * If this flag is `true`, the Sentry plugin will collect some telemetry data and send it to Sentry. - * It will not collect any sensitive or user-specific data. - * - * @default true - * @deprecated Use option `telemetry` instead of `sourceMapsUploadOptions.telemetry` - */ - telemetry?: boolean; - - /** - * Options related to sourcemaps - * - * @deprecated Use option `sourcemaps` instead of `sourceMapsUploadOptions.sourcemaps` - */ - sourcemaps?: { - /** - * A glob or an array of globs that specify the build artifacts and source maps that will be uploaded to Sentry. - * - * If this option is not specified, sensible defaults based on your adapter and nuxt.config.js - * setup will be used. Use this option to override these defaults, for instance if you have a - * customized build setup that diverges from Nuxt's defaults. - * - * The globbing patterns must follow the implementation of the `glob` package. - * @see https://www.npmjs.com/package/glob#glob-primer - * - * @deprecated Use option `sourcemaps.assets` instead of `sourceMapsUploadOptions.sourcemaps.assets` - */ - assets?: string | Array; - - /** - * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. - * - * @default [] - By default no files are ignored. Thus, all files matching the `assets` glob - * or the default value for `assets` are uploaded. - * - * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) - * - * @deprecated Use option `sourcemaps.ignore` instead of `sourceMapsUploadOptions.sourcemaps.ignore` - */ - ignore?: string | Array; - - /** - * A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact - * upload to Sentry has been completed. - * - * @default [] - By default no files are deleted. - * - * The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob) - * - * @deprecated Use option `sourcemaps.filesToDeleteAfterUpload` instead of `sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload` - */ - filesToDeleteAfterUpload?: string | Array; - }; -}; - /** * Build options for the Sentry module. These options are used during build-time by the Sentry SDK. */ @@ -183,17 +33,6 @@ export type SentryNuxtModuleOptions = BuildTimeOptionsBase & { */ enabled?: boolean; - /** - * Options for the Sentry Vite plugin to customize the source maps upload process. - * - * These options are always read from the `sentry` module options in the `nuxt.config.(js|ts). - * Do not define them in the `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files. - * - * @deprecated This option was deprecated as it adds unnecessary nesting. - * Put the options one level higher to the root-level of the `sentry` module options. - */ - sourceMapsUploadOptions?: SourceMapsOptions; - /** * * Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible. diff --git a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts index e16dc5bf4d4a..e1b5b99541b9 100644 --- a/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts @@ -1,5 +1,5 @@ import type { CloudflareOptions } from '@sentry/cloudflare'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; +import { getDefaultIntegrations, setAsyncLocalStorageAsyncContextStrategy } from '@sentry/cloudflare'; import { wrapRequestHandler } from '@sentry/cloudflare/request'; import { debug, getDefaultIsolationScope, getIsolationScope, getTraceData } from '@sentry/core'; import type { H3Event } from 'h3'; @@ -69,7 +69,9 @@ export const sentryCloudflareNitroPlugin = }); const requestHandlerOptions = { - options: cloudflareOptions, + // `wrapRequestHandler` only sets up the integrations that work without `nodejs_compat`. + // Nitro's Cloudflare presets require the flag anyway, so opt into the full set. + options: { defaultIntegrations: getDefaultIntegrations(cloudflareOptions), ...cloudflareOptions }, request, context: getCloudflareProperties(event).context, }; diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index faf68b2bc37d..676e31ca2e9c 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -22,20 +22,9 @@ export function setupSourceMaps( nuxt: Nuxt, addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean }) => void, ): void { - // TODO(v11): remove deprecated options (also from SentryNuxtModuleOptions type) - const isDebug = moduleOptions.debug; - // eslint-disable-next-line typescript/no-deprecated - const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; - - const sourceMapsEnabled = - moduleOptions.sourcemaps?.disable === true - ? false - : moduleOptions.sourcemaps?.disable === false - ? true - : // eslint-disable-next-line typescript/no-deprecated - (sourceMapsUploadOptions.enabled ?? true); + const sourceMapsEnabled = moduleOptions.sourcemaps?.disable !== true; // In case we overwrite the source map settings, we default to deleting the files const shouldDeleteFilesFallback = { client: true, server: true }; @@ -62,11 +51,7 @@ export function setupSourceMaps( ? 'server-side' : 'client-side'; - if ( - !moduleOptions.sourcemaps?.filesToDeleteAfterUpload && - // eslint-disable-next-line typescript/no-deprecated - !sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload - ) { + if (!moduleOptions.sourcemaps?.filesToDeleteAfterUpload) { // eslint-disable-next-line no-console console.log( `[Sentry] We enabled \`'hidden'\` source maps for your ${enabledDeleteFallbacks} build. Source map files will be automatically deleted after uploading them to Sentry.`, @@ -131,65 +116,27 @@ function normalizePath(path: string): string { * * Only exported for Testing purposes. */ -// todo(v11): This "eslint-disable" can be removed again once we remove deprecated options. -// eslint-disable-next-line complexity export function getPluginOptions( moduleOptions: SentryNuxtModuleOptions, shouldDeleteFilesFallback?: { client: boolean; server: boolean }, ): SentryVitePluginOptions | SentryRollupPluginOptions { - // eslint-disable-next-line typescript/no-deprecated - const sourceMapsUploadOptions = moduleOptions.sourceMapsUploadOptions || {}; - - const shouldDeleteFilesAfterUpload = shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server; - const fallbackFilesToDelete = [ - ...(shouldDeleteFilesFallback?.client ? ['.*/**/public/**/*.map'] : []), - ...(shouldDeleteFilesFallback?.server - ? ['.*/**/server/**/*.map', '.*/**/output/**/*.map', '.*/**/function/**/*.map'] - : []), - ]; - - // Check for filesToDeleteAfterUpload in new location first, then deprecated location const sourcemapsOptions = moduleOptions.sourcemaps || {}; - // eslint-disable-next-line typescript/no-deprecated - const deprecatedSourcemapsOptions = sourceMapsUploadOptions.sourcemaps || {}; - - const filesToDeleteAfterUpload = - sourcemapsOptions.filesToDeleteAfterUpload ?? - // eslint-disable-next-line typescript/no-deprecated - deprecatedSourcemapsOptions.filesToDeleteAfterUpload; - - if (typeof filesToDeleteAfterUpload === 'undefined' && shouldDeleteFilesAfterUpload && moduleOptions.debug) { - // eslint-disable-next-line no-console - console.log( - `[Sentry] Setting \`sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [${fallbackFilesToDelete - // Logging it as strings in the array - .map(path => `"${path}"`) - .join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`, - ); - } + const filesToDeleteAfterUpload = resolveFilesToDeleteAfterUpload(moduleOptions, shouldDeleteFilesFallback); return { applicationKey: moduleOptions.applicationKey, - // eslint-disable-next-line typescript/no-deprecated - org: moduleOptions.org ?? sourceMapsUploadOptions.org ?? process.env.SENTRY_ORG, - // eslint-disable-next-line typescript/no-deprecated - project: moduleOptions.project ?? sourceMapsUploadOptions.project ?? process.env.SENTRY_PROJECT, - // eslint-disable-next-line typescript/no-deprecated - authToken: moduleOptions.authToken ?? sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, - // eslint-disable-next-line typescript/no-deprecated - telemetry: moduleOptions.telemetry ?? sourceMapsUploadOptions.telemetry ?? true, - // eslint-disable-next-line typescript/no-deprecated - url: moduleOptions.sentryUrl ?? sourceMapsUploadOptions.url ?? process.env.SENTRY_URL, + org: moduleOptions.org ?? process.env.SENTRY_ORG, + project: moduleOptions.project ?? process.env.SENTRY_PROJECT, + authToken: moduleOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, + telemetry: moduleOptions.telemetry ?? true, + url: moduleOptions.sentryUrl ?? process.env.SENTRY_URL, headers: moduleOptions.headers, debug: moduleOptions.debug ?? false, - // eslint-disable-next-line typescript/no-deprecated - silent: moduleOptions.silent ?? sourceMapsUploadOptions.silent ?? false, - // eslint-disable-next-line typescript/no-deprecated - errorHandler: moduleOptions.errorHandler ?? sourceMapsUploadOptions.errorHandler, + silent: moduleOptions.silent ?? false, + errorHandler: moduleOptions.errorHandler, bundleSizeOptimizations: moduleOptions.bundleSizeOptimizations, // todo: test if this can be overridden by the user release: { - // eslint-disable-next-line typescript/no-deprecated - name: moduleOptions.release?.name ?? sourceMapsUploadOptions.release?.name, + name: moduleOptions.release?.name, // Support all release options from BuildTimeOptionsBase ...moduleOptions.release, ...moduleOptions?.unstable_sentryBundlerPluginOptions?.release, @@ -206,21 +153,54 @@ export function getPluginOptions( // The server/client files are in different places depending on the nitro preset (e.g. '.output/server' or '.netlify/functions-internal/server') // We cannot determine automatically how the build folder looks like (depends on the preset), so we have to accept that source maps are uploaded multiple times (with the vitePlugin for Nuxt and the rollupPlugin for Nitro). // If we could know where the server/client assets are located, we could do something like this (based on the Nitro preset): isNitro ? ['./.output/server/**/*'] : ['./.output/public/**/*'], - // eslint-disable-next-line typescript/no-deprecated - assets: sourcemapsOptions.assets ?? deprecatedSourcemapsOptions.assets ?? undefined, - // eslint-disable-next-line typescript/no-deprecated - ignore: sourcemapsOptions.ignore ?? deprecatedSourcemapsOptions.ignore ?? undefined, - filesToDeleteAfterUpload: filesToDeleteAfterUpload - ? filesToDeleteAfterUpload - : shouldDeleteFilesFallback?.server || shouldDeleteFilesFallback?.client - ? fallbackFilesToDelete - : undefined, + assets: sourcemapsOptions.assets ?? undefined, + ignore: sourcemapsOptions.ignore ?? undefined, + filesToDeleteAfterUpload, rewriteSources: sourcemapsOptions.rewriteSources ?? normalizePath, ...moduleOptions?.unstable_sentryBundlerPluginOptions?.sourcemaps, }, }; } +/** + * Determines which files to delete after upload. If the user set `filesToDeleteAfterUpload`, we use + * that. Otherwise we only delete the source maps Sentry generated itself — i.e. the sides + * (client/server) whose `sourcemap` setting resolved to `undefined`, where Sentry stepped in and + * enabled `'hidden'`. Given Nuxt's default of `{ server: true, client: false }`, this isn't the + * default case: it only kicks in when a side is left `undefined` (whole config or a single sub-key). + * @see https://nuxt.com/docs/4.x/api/nuxt-config#sourcemap + */ +function resolveFilesToDeleteAfterUpload( + moduleOptions: SentryNuxtModuleOptions, + shouldDeleteFilesFallback?: { client: boolean; server: boolean }, +): string | Array | undefined { + const shouldDeleteFilesAfterUpload = shouldDeleteFilesFallback?.client || shouldDeleteFilesFallback?.server; + const fallbackFilesToDelete = [ + ...(shouldDeleteFilesFallback?.client ? ['.*/**/public/**/*.map'] : []), + ...(shouldDeleteFilesFallback?.server + ? ['.*/**/server/**/*.map', '.*/**/output/**/*.map', '.*/**/function/**/*.map'] + : []), + ]; + + const filesToDeleteAfterUpload = moduleOptions.sourcemaps?.filesToDeleteAfterUpload; + + if (typeof filesToDeleteAfterUpload === 'undefined' && shouldDeleteFilesAfterUpload && moduleOptions.debug) { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Setting \`sentry.sourcemaps.filesToDeleteAfterUpload: [${fallbackFilesToDelete + // Logging it as strings in the array + .map(path => `"${path}"`) + .join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`, + ); + } + + return filesToDeleteAfterUpload + ? filesToDeleteAfterUpload + : shouldDeleteFilesAfterUpload + ? fallbackFilesToDelete + : undefined; +} + /* There are multiple ways to set up source maps (https://github.com/getsentry/sentry-javascript/issues/13993 and https://github.com/getsentry/sentry-javascript/pull/15859) 1. User explicitly disabled source maps - keep this setting (emit a warning that errors won't be unminified in Sentry) diff --git a/packages/nuxt/test/vite/buildOptions.test-d.ts b/packages/nuxt/test/vite/buildOptions.test-d.ts index 123a55be5239..5cd219363a8d 100644 --- a/packages/nuxt/test/vite/buildOptions.test-d.ts +++ b/packages/nuxt/test/vite/buildOptions.test-d.ts @@ -73,49 +73,6 @@ describe('Sentry Nuxt build-time options type', () => { expectTypeOf(completeOptions).toEqualTypeOf(); }); - it('includes all deprecated options', () => { - const completeOptions: SentryNuxtModuleOptions = { - // SentryNuxtModuleOptions specific options - enabled: true, - debug: true, - autoInjectServerSentry: 'experimental_dynamic-import', // No need for 'as const' with type assertion - experimental_entrypointWrappedFunctions: ['default', 'handler', 'server', 'customExport'], - unstable_sentryBundlerPluginOptions: { - // Rollup plugin options - bundleSizeOptimizations: { - excludeDebugStatements: true, - }, - // Vite plugin options - sourcemaps: { - assets: './dist/**/*', - }, - }, - - // Deprecated sourceMapsUploadOptions - sourceMapsUploadOptions: { - silent: false, - // eslint-disable-next-line no-console - errorHandler: (err: Error) => console.warn(err), - release: { - name: 'deprecated-release', - }, - enabled: true, - authToken: 'deprecated-token', - org: 'deprecated-org', - url: 'https://deprecated.sentry.io', - project: 'deprecated-project', - telemetry: false, - sourcemaps: { - assets: './build/**/*', - ignore: ['./build/*.spec.js'], - filesToDeleteAfterUpload: ['./build/*.map'], - }, - }, - }; - - expectTypeOf(completeOptions).toEqualTypeOf(); - }); - it('allows partial configuration', () => { const minimalOptions: SentryNuxtModuleOptions = { enabled: true }; diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index a9edb03fb1ed..630789364f20 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -109,6 +109,17 @@ describe('setupSourceMaps hooks', () => { expect(mockAddVitePlugin).not.toHaveBeenCalled(); }); + it('does not add plugins when source maps are disabled via `sourcemaps.disable`', async () => { + const { setupSourceMaps } = await import('../../src/vite/sourceMaps'); + const mockNuxt = createMockNuxt({}); + const { mockAddVitePlugin } = createMockAddVitePlugin(); + + setupSourceMaps({ sourcemaps: { disable: true } }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); + await mockNuxt.triggerHook('modules:done'); + + expect(mockAddVitePlugin).not.toHaveBeenCalled(); + }); + it.each([ { label: 'server (SSR) build', buildConfig: { build: { ssr: true }, plugins: [] } }, { label: 'client build', buildConfig: { build: { ssr: false }, plugins: [] } }, diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts index 28ed1338e510..b5f21b8f7bd3 100644 --- a/packages/nuxt/test/vite/sourceMaps.test.ts +++ b/packages/nuxt/test/vite/sourceMaps.test.ts @@ -79,16 +79,14 @@ describe('getPluginOptions', () => { it('merges custom moduleOptions with default options', () => { const customOptions: SentryNuxtModuleOptions = { - sourceMapsUploadOptions: { - org: 'custom-org', - project: 'custom-project', - authToken: 'custom-token', - telemetry: false, - sourcemaps: { - assets: ['custom-assets/**/*'], - ignore: ['ignore-this.js'], - filesToDeleteAfterUpload: ['delete-this.js'], - }, + org: 'custom-org', + project: 'custom-project', + authToken: 'custom-token', + telemetry: false, + sourcemaps: { + assets: ['custom-assets/**/*'], + ignore: ['ignore-this.js'], + filesToDeleteAfterUpload: ['delete-this.js'], }, debug: true, }; @@ -115,6 +113,35 @@ describe('getPluginOptions', () => { ); }); + it('resolves root-level options into plugin options', () => { + const errorHandler = (err: Error): void => { + throw err; + }; + const result = getPluginOptions({ + org: 'my-org', + project: 'my-project', + authToken: 'my-token', + sentryUrl: 'https://my.sentry.io', + telemetry: false, + silent: true, + errorHandler, + headers: { 'X-Foo': 'bar' }, + release: { name: '1.2.3' }, + } as SentryNuxtModuleOptions); + + expect(result).toMatchObject({ + org: 'my-org', + project: 'my-project', + authToken: 'my-token', + url: 'https://my.sentry.io', // sentryUrl is resolved to the plugin's `url` option + telemetry: false, + silent: true, + errorHandler, + headers: { 'X-Foo': 'bar' }, + release: { name: '1.2.3' }, + }); + }); + it('normalizes source paths via rewriteSources', () => { const options = getPluginOptions({} as SentryNuxtModuleOptions, undefined); const rewrite = options.sourcemaps?.rewriteSources as ((s: string) => string) | undefined; @@ -133,109 +160,6 @@ describe('getPluginOptions', () => { expect(options.sourcemaps?.rewriteSources).toBe(customRewrite); }); - it('prioritizes new BuildTimeOptionsBase options over deprecated ones', () => { - const options: SentryNuxtModuleOptions = { - // New options - org: 'new-org', - project: 'new-project', - authToken: 'new-token', - sentryUrl: 'https://new.sentry.io', - telemetry: false, - silent: true, - debug: true, - sourcemaps: { - assets: ['new-assets/**/*'], - ignore: ['new-ignore.js'], - filesToDeleteAfterUpload: ['new-delete.js'], - }, - release: { - name: 'test-release', - create: false, - finalize: true, - dist: 'build-123', - vcsRemote: 'upstream', - setCommits: { auto: true }, - deploy: { env: 'production' }, - }, - bundleSizeOptimizations: { excludeTracing: true }, - - // Deprecated options (should be ignored) - sourceMapsUploadOptions: { - org: 'old-org', - project: 'old-project', - authToken: 'old-token', - url: 'https://old.sentry.io', - telemetry: true, - silent: false, - sourcemaps: { - assets: ['old-assets/**/*'], - ignore: ['old-ignore.js'], - filesToDeleteAfterUpload: ['old-delete.js'], - }, - release: { name: 'old-release' }, - }, - }; - - const result = getPluginOptions(options); - - expect(result).toMatchObject({ - org: 'new-org', - project: 'new-project', - authToken: 'new-token', - url: 'https://new.sentry.io', - telemetry: false, - silent: true, - debug: true, - bundleSizeOptimizations: { excludeTracing: true }, - release: { - name: 'test-release', - create: false, - finalize: true, - dist: 'build-123', - vcsRemote: 'upstream', - setCommits: { auto: true }, - deploy: { env: 'production' }, - }, - sourcemaps: expect.objectContaining({ - assets: ['new-assets/**/*'], - ignore: ['new-ignore.js'], - filesToDeleteAfterUpload: ['new-delete.js'], - }), - }); - }); - - it('falls back to deprecated options when new ones are undefined', () => { - const options: SentryNuxtModuleOptions = { - debug: true, - sourceMapsUploadOptions: { - org: 'deprecated-org', - project: 'deprecated-project', - authToken: 'deprecated-token', - url: 'https://deprecated.sentry.io', - telemetry: false, - sourcemaps: { - assets: ['deprecated/**/*'], - }, - release: { name: 'deprecated-release' }, - }, - }; - - const result = getPluginOptions(options); - - expect(result).toMatchObject({ - org: 'deprecated-org', - project: 'deprecated-project', - authToken: 'deprecated-token', - url: 'https://deprecated.sentry.io', - telemetry: false, - debug: true, - release: { name: 'deprecated-release' }, - sourcemaps: expect.objectContaining({ - assets: ['deprecated/**/*'], - }), - }); - }); - it('passes applicationKey to plugin options', () => { const options: SentryNuxtModuleOptions = { applicationKey: 'my-app-key', @@ -320,18 +244,7 @@ describe('getPluginOptions', () => { expected: ['.*/**/server/**/*.map', '.*/**/output/**/*.map', '.*/**/function/**/*.map'], }, { - name: 'no fallback, but custom filesToDeleteAfterUpload is provided (deprecated)', - clientFallback: false, - serverFallback: false, - customOptions: { - sourceMapsUploadOptions: { - sourcemaps: { filesToDeleteAfterUpload: ['deprecated/path/**/*.map'] }, - }, - }, - expected: ['deprecated/path/**/*.map'], - }, - { - name: 'no fallback, but custom filesToDeleteAfterUpload is provided (new)', + name: 'no fallback, but custom filesToDeleteAfterUpload is provided', clientFallback: false, serverFallback: false, customOptions: { diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index e13b62087aee..2eb36a6c7d77 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -2,10 +2,13 @@ import * as api from '@opentelemetry/api'; import type { Scope, TracingChannelBinding } from '@sentry/core'; import { getAsyncContextStrategy, + _INTERNAL_safeMathRandom, + generateTraceId, getDefaultCurrentScope, getDefaultIsolationScope, getMainCarrier, getRootSpan, + isContinuingTrace, setAsyncContextStrategy, spanIsIgnored, } from '@sentry/core'; @@ -86,6 +89,19 @@ export function setOpenTelemetryContextAsyncContextStrategy(): AsyncLocalStorage // the OTEL context manager, which uses the presence of this key to determine if it should // fork the isolation scope, or not return api.context.with(ctx.setValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, true), () => { + // When forking an isolation scope, unless we are continuing an incoming + // trace, we give the freshly forked scope its own trace. This way, new + // root spans in an isolation scope will get separate traces. The + // previous trace's `sampled` and `propagationSpanId` are dropped on + // purpose. Carrying them over would apply the old trace's sampling + // decision to the new one and propagate a span id from a different trace. + const scope = getCurrentScope(); + if (!isContinuingTrace(scope.getPropagationContext())) { + scope.setPropagationContext({ + traceId: generateTraceId(), + sampleRand: _INTERNAL_safeMathRandom(), + }); + } return callback(getIsolationScope()); }); } diff --git a/packages/opentelemetry/test/asyncContextStrategy.test.ts b/packages/opentelemetry/test/asyncContextStrategy.test.ts index a36ae15180a7..bd9d4956b895 100644 --- a/packages/opentelemetry/test/asyncContextStrategy.test.ts +++ b/packages/opentelemetry/test/asyncContextStrategy.test.ts @@ -19,6 +19,15 @@ import { TraceState } from '../src/utils/TraceState'; import { mockSdkInit } from './helpers/mockSdkInit'; describe('asyncContextStrategy', () => { + // `withIsolationScope` gives the forked current scope a fresh propagation context (unless it is + // continuing an incoming trace), so scope data is expected to match apart from that context. + function scopeDataWithoutPropagationContext( + scope: Scope, + ): Omit, 'propagationContext'> { + const { propagationContext: _propagationContext, ...rest } = scope.getScopeData(); + return rest; + } + beforeEach(() => { getCurrentScope().clear(); getIsolationScope().clear(); @@ -45,7 +54,8 @@ describe('asyncContextStrategy', () => { expect(scope1).not.toBe(initialScope); expect(isolationScope1).not.toBe(initialIsolationScope); - expect(scope1.getScopeData()).toEqual(initialScope.getScopeData()); + expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope)); + expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId); expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData()); scope1.setExtra('b', 'b'); @@ -162,7 +172,8 @@ describe('asyncContextStrategy', () => { expect(scope1).not.toBe(initialScope); expect(isolationScope1).not.toBe(initialIsolationScope); - expect(scope1.getScopeData()).toEqual(initialScope.getScopeData()); + expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope)); + expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId); expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData()); await asyncSetExtra(scope1, 'b', 'b'); @@ -207,7 +218,8 @@ describe('asyncContextStrategy', () => { expect(scope1).not.toBe(initialScope); expect(isolationScope1).not.toBe(initialIsolationScope); - expect(scope1.getScopeData()).toEqual(initialScope.getScopeData()); + expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope)); + expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId); expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData()); scope1.setExtra('b', 'b'); @@ -244,7 +256,8 @@ describe('asyncContextStrategy', () => { expect(scope1).not.toBe(initialScope); expect(isolationScope1).not.toBe(initialIsolationScope); - expect(scope1.getScopeData()).toEqual(initialScope.getScopeData()); + expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope)); + expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId); expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData()); scope1.setExtra('b2', 'b'); @@ -294,7 +307,8 @@ describe('asyncContextStrategy', () => { expect(scope1).not.toBe(initialScope); expect(isolationScope1).not.toBe(initialIsolationScope); - expect(scope1.getScopeData()).toEqual(initialScope.getScopeData()); + expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope)); + expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId); expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData()); await asyncSetExtra(scope1, 'b', 'b'); @@ -331,7 +345,8 @@ describe('asyncContextStrategy', () => { expect(scope1).not.toBe(initialScope); expect(isolationScope1).not.toBe(initialIsolationScope); - expect(scope1.getScopeData()).toEqual(initialScope.getScopeData()); + expect(scopeDataWithoutPropagationContext(scope1)).toEqual(scopeDataWithoutPropagationContext(initialScope)); + expect(scope1.getPropagationContext().traceId).not.toBe(initialScope.getPropagationContext().traceId); expect(isolationScope1.getScopeData()).toEqual(initialIsolationScope.getScopeData()); scope1.setExtra('b2', 'b'); @@ -525,5 +540,50 @@ describe('asyncContextStrategy', () => { done(); }); })); + + // A trace-id-only `sentry-trace` header yields a propagation context with + // a `dsc` but no `parentSpanId`, because the span id is optional in the + // header. Such a trace is still being continued, so its trace id must + // survive the fork. + it('keeps the trace id when continuing an incoming trace without a span id (dsc set)', () => { + const incomingTraceId = 'cafecafecafecafecafecafecafecafe'; + getCurrentScope().setPropagationContext({ + traceId: incomingTraceId, + sampleRand: 0.42, + dsc: { trace_id: incomingTraceId, sample_rate: '1' }, + }); + + withIsolationScope(() => { + const propagationContext = getCurrentScope().getPropagationContext(); + + expect(propagationContext.traceId).toBe(incomingTraceId); + expect(propagationContext.dsc).toEqual({ trace_id: incomingTraceId, sample_rate: '1' }); + }); + }); + + // A new trace must not inherit the previous trace's sampling decision or + // propagation span id. Keeping them would apply the old trace's sampling + // decision to the new one and propagate a span id belonging to a + // different trace. + it('drops the previous trace data when giving a forked isolation scope its own trace', () => { + const oldTraceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + getCurrentScope().setPropagationContext({ + traceId: oldTraceId, + sampleRand: 0.1, + sampled: true, + propagationSpanId: 'bbbbbbbbbbbbbbbb', + }); + + withIsolationScope(() => { + const propagationContext = getCurrentScope().getPropagationContext(); + + expect(propagationContext.traceId).toMatch(/^[a-f0-9]{32}$/); + expect(propagationContext.traceId).not.toBe(oldTraceId); + expect(propagationContext.sampleRand).toEqual(expect.any(Number)); + expect(propagationContext.sampled).toBeUndefined(); + expect(propagationContext.propagationSpanId).toBeUndefined(); + expect(propagationContext.dsc).toBeUndefined(); + }); + }); }); }); diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 19644cf985e2..0662bc4a2b3a 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -547,8 +547,7 @@ class ContinuousProfiler { return; } - const traceId = - getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId; + const traceId = getCurrentScope().getPropagationContext().traceId; const chunk = this._initializeChunk(traceId); CpuProfilerBindings.startProfiling(chunk.id); diff --git a/packages/server-utils/src/ai/core/gen-ai-attributes.ts b/packages/server-utils/src/ai/core/gen-ai-attributes.ts new file mode 100644 index 000000000000..671dfd042165 --- /dev/null +++ b/packages/server-utils/src/ai/core/gen-ai-attributes.ts @@ -0,0 +1,51 @@ +/** + * Gen-AI telemetry attributes that are not (yet) covered by `@sentry/conventions`. + * + * Attributes with an equivalent in `@sentry/conventions/attributes` are imported from there directly + * at their call sites. The constants below either have no conventions equivalent, are Sentry-internal + * meta attributes, are span-operation values (not attribute keys), or intentionally emit a different + * key than the current conventions attribute. + * + * Based on OpenTelemetry Semantic Conventions for Generative AI + * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/ + */ + +/** + * Whether streaming was enabled for the request + */ +export const GEN_AI_REQUEST_STREAM_ATTRIBUTE = 'gen_ai.request.stream'; + +/** + * The encoding format for the model request + */ +export const GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE = 'gen_ai.request.encoding_format'; + +/** + * The dimensions for the model request + */ +export const GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE = 'gen_ai.request.dimensions'; + +/** + * The reason why the model stopped generating tokens + */ +export const GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE = 'gen_ai.response.stop_reason'; + +/** + * The span operation name for invoking an agent + */ +export const GEN_AI_INVOKE_AGENT_OPERATION_ATTRIBUTE = 'gen_ai.invoke_agent'; + +/** + * The span operation for embeddings + */ +export const GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE = 'gen_ai.embeddings'; + +/** + * The span operation name for executing a tool + */ +export const GEN_AI_EXECUTE_TOOL_OPERATION_ATTRIBUTE = 'gen_ai.execute_tool'; + +/** + * The tool call ID + */ +export const GEN_AI_TOOL_CALL_ID_ATTRIBUTE = 'gen_ai.tool.call.id'; diff --git a/packages/server-utils/src/ai/core/mediaStripping.ts b/packages/server-utils/src/ai/core/mediaStripping.ts new file mode 100644 index 000000000000..cb8e5d7b959e --- /dev/null +++ b/packages/server-utils/src/ai/core/mediaStripping.ts @@ -0,0 +1,197 @@ +/** + * Inline media content source, with a potentially very large base64 + * blob or data: uri. + */ +export type ContentMedia = Record & + ( + | { + media_type: string; + data: string; + } + | { + image_url: `data:${string}`; + } + | { + image_url: { url: `data:${string}` }; + } + | { + type: 'blob' | 'base64'; + content: string; + } + | { + b64_json: string; + } + | { + uri: `data:${string}`; + } + | { + type: 'input_audio'; + input_audio: { data: string }; + } + | { + type: 'file'; + file: { file_data?: string }; + } + ); + +/** + * Check if a content part is an OpenAI/Anthropic media source + */ +export function isContentMedia(part: unknown): part is ContentMedia { + if (!part || typeof part !== 'object') return false; + + return ( + isContentMediaSource(part) || + hasInlineData(part) || + hasImageUrl(part) || + hasInputAudio(part) || + hasFileData(part) || + hasMediaTypeData(part) || + hasVercelFileData(part) || + hasVercelImageData(part) || + hasBlobOrBase64Type(part) || + hasB64Json(part) || + hasImageGenerationResult(part) || + hasDataUri(part) + ); +} + +function hasImageUrl(part: NonNullable): boolean { + if (!('image_url' in part)) return false; + if (typeof part.image_url === 'string') return part.image_url.startsWith('data:'); + return hasNestedImageUrl(part); +} + +function hasNestedImageUrl(part: NonNullable): part is { image_url: { url: string } } { + return ( + 'image_url' in part && + !!part.image_url && + typeof part.image_url === 'object' && + 'url' in part.image_url && + typeof part.image_url.url === 'string' && + part.image_url.url.startsWith('data:') + ); +} + +function isContentMediaSource(part: NonNullable): boolean { + return 'type' in part && typeof part.type === 'string' && 'source' in part && isContentMedia(part.source); +} + +function hasInlineData(part: NonNullable): part is { inlineData: { data?: string } } { + return ( + 'inlineData' in part && + !!part.inlineData && + typeof part.inlineData === 'object' && + 'data' in part.inlineData && + typeof part.inlineData.data === 'string' + ); +} + +function hasInputAudio(part: NonNullable): part is { type: 'input_audio'; input_audio: { data: string } } { + return ( + 'type' in part && + part.type === 'input_audio' && + 'input_audio' in part && + !!part.input_audio && + typeof part.input_audio === 'object' && + 'data' in part.input_audio && + typeof part.input_audio.data === 'string' + ); +} + +function hasFileData(part: NonNullable): part is { type: 'file'; file: { file_data: string } } { + return ( + 'type' in part && + part.type === 'file' && + 'file' in part && + !!part.file && + typeof part.file === 'object' && + 'file_data' in part.file && + typeof part.file.file_data === 'string' + ); +} + +function hasMediaTypeData(part: NonNullable): part is { media_type: string; data: string } { + return 'media_type' in part && typeof part.media_type === 'string' && 'data' in part; +} + +/** + * Check for Vercel AI SDK file format: { type: "file", mediaType: "...", data: "..." } + * Only matches base64/binary data, not HTTP/HTTPS URLs (which should be preserved). + */ +function hasVercelFileData(part: NonNullable): part is { type: 'file'; mediaType: string; data: string } { + return ( + 'type' in part && + part.type === 'file' && + 'mediaType' in part && + typeof part.mediaType === 'string' && + 'data' in part && + typeof part.data === 'string' && + // Only strip base64/binary data, not HTTP/HTTPS URLs which should be preserved as references + !part.data.startsWith('http://') && + !part.data.startsWith('https://') + ); +} + +/** + * Check for Vercel AI SDK image format: { type: "image", image: "base64...", mimeType?: "..." } + * Only matches base64/data URIs, not HTTP/HTTPS URLs (which should be preserved). + * Note: mimeType is optional in Vercel AI SDK image parts. + */ +function hasVercelImageData(part: NonNullable): part is { type: 'image'; image: string; mimeType?: string } { + return ( + 'type' in part && + part.type === 'image' && + 'image' in part && + typeof part.image === 'string' && + // Only strip base64/data URIs, not HTTP/HTTPS URLs which should be preserved as references + !part.image.startsWith('http://') && + !part.image.startsWith('https://') + ); +} + +function hasBlobOrBase64Type(part: NonNullable): part is { type: 'blob' | 'base64'; content: string } { + return 'type' in part && (part.type === 'blob' || part.type === 'base64'); +} + +function hasB64Json(part: NonNullable): part is { b64_json: string } { + return 'b64_json' in part; +} + +function hasImageGenerationResult(part: NonNullable): part is { type: 'image_generation'; result: string } { + return 'type' in part && 'result' in part && part.type === 'image_generation'; +} + +function hasDataUri(part: NonNullable): part is { uri: string } { + return 'uri' in part && typeof part.uri === 'string' && part.uri.startsWith('data:'); +} + +const REMOVED_STRING = '[Blob substitute]'; + +const MEDIA_FIELDS = ['image_url', 'data', 'content', 'b64_json', 'result', 'uri', 'image'] as const; + +/** + * Replace inline binary data in a single media content part with a placeholder. + */ +export function stripInlineMediaFromSingleMessage(part: ContentMedia): ContentMedia { + const strip = { ...part }; + if (isContentMedia(strip.source)) { + strip.source = stripInlineMediaFromSingleMessage(strip.source); + } + if (hasInlineData(part)) { + strip.inlineData = { ...part.inlineData, data: REMOVED_STRING }; + } + if (hasNestedImageUrl(part)) { + strip.image_url = { ...part.image_url, url: REMOVED_STRING }; + } + if (hasInputAudio(part)) { + strip.input_audio = { ...part.input_audio, data: REMOVED_STRING }; + } + if (hasFileData(part)) { + strip.file = { ...part.file, file_data: REMOVED_STRING }; + } + for (const field of MEDIA_FIELDS) { + if (typeof strip[field] === 'string') strip[field] = REMOVED_STRING; + } + return strip; +} diff --git a/packages/server-utils/src/ai/core/messageTruncation.ts b/packages/server-utils/src/ai/core/messageTruncation.ts new file mode 100644 index 000000000000..779cf332855b --- /dev/null +++ b/packages/server-utils/src/ai/core/messageTruncation.ts @@ -0,0 +1,409 @@ +import { isContentMedia, stripInlineMediaFromSingleMessage } from './mediaStripping'; + +/** + * Default maximum size in bytes for GenAI messages. + * Messages exceeding this limit will be truncated. + */ +export const DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT = 20000; + +/** + * Message format used by OpenAI and Anthropic APIs. + */ +type ContentMessage = { + [key: string]: unknown; + content: string; +}; + +/** + * One block inside OpenAI / Anthropic `content: [...]` arrays (text, image_url, etc.). + */ +type ContentArrayBlock = { + [key: string]: unknown; + type: string; +}; + +/** + * Message format used by OpenAI and Anthropic APIs for media. + */ +type ContentArrayMessage = { + [key: string]: unknown; + content: ContentArrayBlock[]; +}; + +/** + * Message format used by Google GenAI API. + * Parts can be strings or objects with a text property. + */ +type PartsMessage = { + [key: string]: unknown; + parts: Array; +}; + +/** + * A part in a Google GenAI message that contains text. + */ +type TextPart = string | { text: string }; + +/** + * A part in a Google GenAI that contains media. + */ +type MediaPart = { + type: string; + content: string; +}; + +/** + * One element of an array-based message: OpenAI/Anthropic `content[]` or Google `parts`. + */ +type ArrayMessageItem = TextPart | MediaPart | ContentArrayBlock; + +/** + * Calculate the UTF-8 byte length of a string. + */ +const utf8Bytes = (text: string): number => { + return new TextEncoder().encode(text).length; +}; + +/** + * Calculate the UTF-8 byte length of a value's JSON representation. + */ +const jsonBytes = (value: unknown): number => { + return utf8Bytes(JSON.stringify(value)); +}; + +/** + * Truncate a string to fit within maxBytes (inclusive) when encoded as UTF-8. + * Uses binary search for efficiency with multi-byte characters. + * + * @param text - The string to truncate + * @param maxBytes - Maximum byte length (inclusive, UTF-8 encoded) + * @returns Truncated string whose UTF-8 byte length is at most maxBytes + */ +function truncateTextByBytes(text: string, maxBytes: number): string { + if (utf8Bytes(text) <= maxBytes) { + return text; + } + + let low = 0; + let high = text.length; + let bestFit = ''; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const candidate = text.slice(0, mid); + const byteSize = utf8Bytes(candidate); + + if (byteSize <= maxBytes) { + bestFit = candidate; + low = mid + 1; + } else { + high = mid - 1; + } + } + + return bestFit; +} + +/** + * Extract text content from a message item. + * Handles plain strings and objects with a text property. + * + * @returns The text content + */ +function getItemText(item: ArrayMessageItem): string { + if (typeof item === 'string') { + return item; + } + if ('text' in item && typeof item.text === 'string') { + return item.text; + } + return ''; +} + +/** + * Create a new item with updated text content while preserving the original structure. + * + * @param item - Original item (string or object) + * @param text - New text content + * @returns New item with updated text + */ +function withItemText(item: ArrayMessageItem, text: string): ArrayMessageItem { + if (typeof item === 'string') { + return text; + } + return { ...item, text }; +} + +/** + * Check if a message has the OpenAI/Anthropic content format. + */ +function isContentMessage(message: unknown): message is ContentMessage { + return ( + message !== null && + typeof message === 'object' && + 'content' in message && + typeof (message as ContentMessage).content === 'string' + ); +} + +/** + * Check if a message has the OpenAI/Anthropic content array format. + */ +function isContentArrayMessage(message: unknown): message is ContentArrayMessage { + return message !== null && typeof message === 'object' && 'content' in message && Array.isArray(message.content); +} + +/** + * Check if a message has the Google GenAI parts format. + */ +function isPartsMessage(message: unknown): message is PartsMessage { + return ( + message !== null && + typeof message === 'object' && + 'parts' in message && + Array.isArray((message as PartsMessage).parts) && + (message as PartsMessage).parts.length > 0 + ); +} + +/** + * Truncate a message with `content: string` format (OpenAI/Anthropic). + * + * @param message - Message with content property + * @param maxBytes - Maximum byte limit + * @returns Array with truncated message, or empty array if it doesn't fit + */ +function truncateContentMessage(message: ContentMessage, maxBytes: number): unknown[] { + // Calculate overhead (message structure without content) + const emptyMessage = { ...message, content: '' }; + const overhead = jsonBytes(emptyMessage); + const availableForContent = maxBytes - overhead; + + if (availableForContent <= 0) { + return []; + } + + const truncatedContent = truncateTextByBytes(message.content, availableForContent); + return [{ ...message, content: truncatedContent }]; +} + +/** + * Extracts the array items and their key from an array-based message. + * Returns `null` key if neither `parts` nor `content` is a valid array. + */ +function getArrayItems(message: PartsMessage | ContentArrayMessage): { + key: 'parts' | 'content' | null; + items: ArrayMessageItem[]; +} { + if ('parts' in message && Array.isArray(message.parts)) { + return { key: 'parts', items: message.parts }; + } + if ('content' in message && Array.isArray(message.content)) { + return { key: 'content', items: message.content }; + } + return { key: null, items: [] }; +} + +/** + * Truncate a message with an array-based format. + * Handles both `parts: [...]` (Google GenAI) and `content: [...]` (OpenAI/Anthropic multimodal). + * Keeps as many complete items as possible, only truncating the first item if needed. + * + * @param message - Message with parts or content array + * @param maxBytes - Maximum byte limit + * @returns Array with truncated message, or empty array if it doesn't fit + */ +function truncateArrayMessage(message: PartsMessage | ContentArrayMessage, maxBytes: number): unknown[] { + const { key, items } = getArrayItems(message); + + if (key === null || items.length === 0) { + return []; + } + + // Calculate overhead by creating empty text items + const emptyItems = items.map(item => withItemText(item, '')); + const overhead = jsonBytes({ ...message, [key]: emptyItems }); + let remainingBytes = maxBytes - overhead; + + if (remainingBytes <= 0) { + return []; + } + + // Include items until we run out of space + const includedItems: ArrayMessageItem[] = []; + + for (const item of items) { + const text = getItemText(item); + const textSize = utf8Bytes(text); + + if (textSize <= remainingBytes) { + // Item fits: include it as-is + includedItems.push(item); + remainingBytes -= textSize; + } else if (includedItems.length === 0) { + // First item doesn't fit: truncate it + const truncated = truncateTextByBytes(text, remainingBytes); + if (truncated) { + includedItems.push(withItemText(item, truncated)); + } + break; + } else { + // Subsequent item doesn't fit: stop here + break; + } + } + + /* c8 ignore start + * for type safety only, algorithm guarantees SOME text included */ + if (includedItems.length <= 0) { + return []; + } else { + /* c8 ignore stop */ + return [{ ...message, [key]: includedItems }]; + } +} + +/** + * Truncate a single message to fit within maxBytes. + * + * Supports three message formats: + * - OpenAI/Anthropic: `{ ..., content: string }` + * - Vercel AI/OpenAI multimodal: `{ ..., content: Array<{type, text?, ...}> }` + * - Google GenAI: `{ ..., parts: Array }` + * + * @param message - The message to truncate + * @param maxBytes - Maximum byte limit for the message + * @returns Array containing the truncated message, or empty array if truncation fails + */ +function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { + if (!message) return []; + + // Handle plain strings (e.g., embeddings input) + if (typeof message === 'string') { + const truncated = truncateTextByBytes(message, maxBytes); + return truncated ? [truncated] : []; + } + + if (typeof message !== 'object') { + return []; + } + + if (isContentMessage(message)) { + return truncateContentMessage(message, maxBytes); + } + + if (isContentArrayMessage(message) || isPartsMessage(message)) { + return truncateArrayMessage(message, maxBytes); + } + + // Unknown message format: cannot truncate safely + return []; +} + +/** + * Strip the inline media from message arrays. + * + * This returns a stripped message. We do NOT want to mutate the data in place, + * because of course we still want the actual API/client to handle the media. + */ +function stripInlineMediaFromMessages(messages: unknown[]): unknown[] { + const stripped = messages.map(message => { + let newMessage: Record | undefined = undefined; + if (!!message && typeof message === 'object') { + if (isContentArrayMessage(message)) { + newMessage = { + ...message, + content: stripInlineMediaFromMessages(message.content), + }; + } else if ('content' in message && isContentMedia(message.content)) { + newMessage = { + ...message, + content: stripInlineMediaFromSingleMessage(message.content), + }; + } + if (isPartsMessage(message)) { + newMessage = { + // might have to strip content AND parts + ...(newMessage ?? message), + parts: stripInlineMediaFromMessages(message.parts), + }; + } + if (isContentMedia(newMessage)) { + newMessage = stripInlineMediaFromSingleMessage(newMessage); + } else if (isContentMedia(message)) { + newMessage = stripInlineMediaFromSingleMessage(message); + } + } + return newMessage ?? message; + }); + return stripped; +} + +/** + * Truncate an array of messages to fit within a byte limit. + * + * Strategy: + * - Always keeps only the last (newest) message + * - Strips inline media from the message + * - Truncates the message content if it exceeds the byte limit + * + * @param messages - Array of messages to truncate + * @param maxBytes - Maximum total byte limit for the message + * @returns Array containing only the last message (possibly truncated) + * + * @example + * ```ts + * const messages = [msg1, msg2, msg3, msg4]; // newest is msg4 + * const truncated = truncateMessagesByBytes(messages, 10000); + * // Returns [msg4] (truncated if needed) + * ``` + */ +function truncateMessagesByBytes(messages: unknown[], maxBytes: number): unknown[] { + // Early return for empty or invalid input + if (!Array.isArray(messages) || messages.length === 0) { + return messages; + } + + // The result is always a single-element array that callers wrap with + // JSON.stringify([message]), so subtract the 2-byte array wrapper ("[" and "]") + // to ensure the final serialized value stays under the limit. + const effectiveMaxBytes = maxBytes - 2; + + // Always keep only the last message + const lastMessage = messages[messages.length - 1]; + + // Strip inline media from the single message + const stripped = stripInlineMediaFromMessages([lastMessage]); + const strippedMessage = stripped[0]; + + // Check if it fits + const messageBytes = jsonBytes(strippedMessage); + if (messageBytes <= effectiveMaxBytes) { + return stripped; + } + + // Truncate the single message if needed + return truncateSingleMessage(strippedMessage, effectiveMaxBytes); +} + +/** + * Truncate GenAI messages using the default byte limit. + * + * Convenience wrapper around `truncateMessagesByBytes` with the default limit. + * + * @param messages - Array of messages to truncate + * @returns Truncated array of messages + */ +export function truncateGenAiMessages(messages: unknown[]): unknown[] { + return truncateMessagesByBytes(messages, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT); +} + +/** + * Truncate GenAI string input using the default byte limit. + * + * @param input - The string to truncate + * @returns Truncated string + */ +export function truncateGenAiStringInput(input: string): string { + return truncateTextByBytes(input, DEFAULT_GEN_AI_MESSAGES_BYTE_LIMIT); +} diff --git a/packages/server-utils/src/ai/core/utils.ts b/packages/server-utils/src/ai/core/utils.ts new file mode 100644 index 000000000000..6b877cc8a067 --- /dev/null +++ b/packages/server-utils/src/ai/core/utils.ts @@ -0,0 +1,326 @@ +/* eslint-disable typescript-eslint/no-deprecated */ +/** + * Shared utils for AI integrations (OpenAI, Anthropic, Verce.AI, etc.) + */ +import { captureException } from '@sentry/core'; +import { getClient } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { isThenable } from '@sentry/core'; +import { + GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_RESPONSE_TEXT, + GEN_AI_RESPONSE_TOOL_CALLS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { truncateGenAiMessages, truncateGenAiStringInput } from './messageTruncation'; + +export interface AIRecordingOptions { + recordInputs?: boolean; + recordOutputs?: boolean; +} + +/** + * A method registry entry describes a single instrumented method: + * which gen_ai operation it maps to and whether it is intrinsically streaming. + */ +export interface InstrumentedMethodEntry { + /** Operation name (e.g. 'chat', 'embeddings', 'generate_content'). Omit for factory methods that only need result proxying. */ + operation?: string; + /** True if the method itself is always streaming (not param-based) */ + streaming?: boolean; + /** When set, the method's return value is re-proxied with this as the base path */ + proxyResultPath?: string; +} + +/** + * Maps method paths to their registry entries. + * Used by proxy-based AI client instrumentations to determine which methods + * to instrument, what operation name to use, and whether they stream. + */ +export type InstrumentedMethodRegistry = Record; + +/** + * Resolves AI recording options by falling back to the client's `dataCollection.genAI` settings. + * Precedence: explicit option > dataCollection.genAI > true (genAI data collected by default) + */ +export function resolveAIRecordingOptions(options?: T): T & Required { + const genAI = getClient()?.getDataCollectionOptions().genAI; + return { + ...options, + recordInputs: options?.recordInputs ?? genAI?.inputs ?? true, + recordOutputs: options?.recordOutputs ?? genAI?.outputs ?? true, + } as T & Required; +} + +/** + * Resolves whether truncation should be enabled. + * If the user explicitly set `enableTruncation`, that value is used. + * Otherwise, truncation is disabled because gen_ai spans are always sent through the v2 span path + * (full span streaming via `traceLifecycle: 'stream'`, or extraction into a v2 span envelope for + * static transactions). That path is not subject to the transaction payload-size limits that + * truncation works around, so the full message data can be retained. + */ +export function shouldEnableTruncation(enableTruncation: boolean | undefined): boolean { + if (enableTruncation !== undefined) { + return enableTruncation; + } + + return !getClient(); +} + +/** + * Build method path from current traversal + */ +export function buildMethodPath(currentPath: string, prop: string): string { + return currentPath ? `${currentPath}.${prop}` : prop; +} + +/** + * Set token usage attributes + * @param span - The span to add attributes to + * @param promptTokens - The number of prompt tokens + * @param completionTokens - The number of completion tokens + * @param cachedInputTokens - The number of cached input tokens + * @param cachedOutputTokens - The number of cached output tokens + */ +export function setTokenUsageAttributes( + span: Span, + promptTokens?: number, + completionTokens?: number, + cachedInputTokens?: number, + cachedOutputTokens?: number, +): void { + if (promptTokens !== undefined) { + span.setAttributes({ + [GEN_AI_USAGE_INPUT_TOKENS]: promptTokens, + }); + } + if (completionTokens !== undefined) { + span.setAttributes({ + [GEN_AI_USAGE_OUTPUT_TOKENS]: completionTokens, + }); + } + if ( + promptTokens !== undefined || + completionTokens !== undefined || + cachedInputTokens !== undefined || + cachedOutputTokens !== undefined + ) { + /** + * Total input tokens in a request is the summation of `input_tokens`, + * `cache_creation_input_tokens`, and `cache_read_input_tokens`. + */ + const totalTokens = + (promptTokens ?? 0) + (completionTokens ?? 0) + (cachedInputTokens ?? 0) + (cachedOutputTokens ?? 0); + + span.setAttributes({ + [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, + }); + } +} + +export interface StreamResponseState { + responseId?: string; + responseModel?: string; + finishReasons: string[]; + responseTexts: string[]; + toolCalls: unknown[]; + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + cacheCreationInputTokens?: number; + cacheReadInputTokens?: number; +} + +/** + * Ends a streaming span by setting all accumulated response attributes and ending the span. + * Shared across OpenAI, Anthropic, and Google GenAI streaming implementations. + */ +export function endStreamSpan(span: Span, state: StreamResponseState, recordOutputs: boolean): void { + if (!span.isRecording()) { + return; + } + + const attrs: Record = { + [GEN_AI_RESPONSE_STREAMING]: true, + }; + + if (state.responseId) attrs[GEN_AI_RESPONSE_ID] = state.responseId; + if (state.responseModel) attrs[GEN_AI_RESPONSE_MODEL] = state.responseModel; + + if (state.promptTokens !== undefined) attrs[GEN_AI_USAGE_INPUT_TOKENS] = state.promptTokens; + if (state.completionTokens !== undefined) attrs[GEN_AI_USAGE_OUTPUT_TOKENS] = state.completionTokens; + + // Use explicit total if provided (OpenAI, Google), otherwise compute from cache tokens (Anthropic) + if (state.totalTokens !== undefined) { + attrs[GEN_AI_USAGE_TOTAL_TOKENS] = state.totalTokens; + } else if ( + state.promptTokens !== undefined || + state.completionTokens !== undefined || + state.cacheCreationInputTokens !== undefined || + state.cacheReadInputTokens !== undefined + ) { + attrs[GEN_AI_USAGE_TOTAL_TOKENS] = + (state.promptTokens ?? 0) + + (state.completionTokens ?? 0) + + (state.cacheCreationInputTokens ?? 0) + + (state.cacheReadInputTokens ?? 0); + } + + if (state.finishReasons.length) { + attrs[GEN_AI_RESPONSE_FINISH_REASONS] = JSON.stringify(state.finishReasons); + } + if (recordOutputs && state.responseTexts.length) { + attrs[GEN_AI_RESPONSE_TEXT] = state.responseTexts.join(''); + } + if (recordOutputs && state.toolCalls.length) { + attrs[GEN_AI_RESPONSE_TOOL_CALLS] = JSON.stringify(state.toolCalls); + } + + span.setAttributes(attrs); + span.end(); +} + +/** + * Get the truncated JSON string for a string, an array of messages, or an object. + * + * @param value - The value to truncate and serialize + * @returns The truncated JSON string + */ +export function getTruncatedJsonString(value: T | T[]): string { + if (typeof value === 'string') { + // Some values are already JSON strings, so we don't need to duplicate the JSON parsing + return truncateGenAiStringInput(value); + } + // Both truncation (media stripping recurses the value) and `JSON.stringify` can throw on + // circular refs or non-serializable values (e.g. BigInt); never let that crash instrumentation. + try { + return JSON.stringify(Array.isArray(value) ? truncateGenAiMessages(value) : value); + } catch { + return '[unserializable]'; + } +} + +/** + * Extract system instructions from messages array. + * Finds the first system message and formats it according to OpenTelemetry semantic conventions. + * + * @param messages - Array of messages to extract system instructions from + * @returns systemInstructions (JSON string) and filteredMessages (without system message) + */ +export function extractSystemInstructions(messages: unknown[] | unknown): { + systemInstructions: string | undefined; + filteredMessages: unknown[] | unknown; +} { + if (!Array.isArray(messages)) { + return { systemInstructions: undefined, filteredMessages: messages }; + } + + const systemMessageIndex = messages.findIndex( + msg => msg && typeof msg === 'object' && 'role' in msg && (msg as { role: string }).role === 'system', + ); + + if (systemMessageIndex === -1) { + return { systemInstructions: undefined, filteredMessages: messages }; + } + + const systemMessage = messages[systemMessageIndex] as { role: string; content?: string | unknown }; + const systemContent = + typeof systemMessage.content === 'string' + ? systemMessage.content + : systemMessage.content !== undefined + ? JSON.stringify(systemMessage.content) + : undefined; + + if (!systemContent) { + return { systemInstructions: undefined, filteredMessages: messages }; + } + + const systemInstructions = JSON.stringify([{ type: 'text', content: systemContent }]); + const filteredMessages = [...messages.slice(0, systemMessageIndex), ...messages.slice(systemMessageIndex + 1)]; + + return { systemInstructions, filteredMessages }; +} + +/** + * Creates a wrapped version of .withResponse() that replaces the data field + * with the instrumented result while preserving metadata (response, request_id). + */ +async function createWithResponseWrapper( + originalWithResponse: Promise, + instrumentedPromise: Promise, + mechanismType: string, +): Promise { + // Attach catch handler to originalWithResponse immediately to prevent unhandled rejection + // If instrumentedPromise rejects first, we still need this handled + const safeOriginalWithResponse = originalWithResponse.catch(error => { + captureException(error, { + mechanism: { + handled: false, + type: mechanismType, + }, + }); + throw error; + }); + + const instrumentedResult = await instrumentedPromise; + const originalWrapper = await safeOriginalWithResponse; + + // Combine instrumented result with original metadata + if (originalWrapper && typeof originalWrapper === 'object' && 'data' in originalWrapper) { + return { + ...originalWrapper, + data: instrumentedResult, + }; + } + return instrumentedResult; +} + +/** + * Wraps a promise-like object to preserve additional methods (like .withResponse()) + * that AI SDK clients (OpenAI, Anthropic) attach to their APIPromise return values. + * + * Standard Promise methods (.then, .catch, .finally) are routed to the instrumented + * promise to preserve Sentry's span instrumentation, while custom SDK methods are + * forwarded to the original promise to maintain the SDK's API surface. + */ +export function wrapPromiseWithMethods( + originalPromiseLike: Promise, + instrumentedPromise: Promise, + mechanismType: string, +): Promise { + // If the original result is not thenable, return the instrumented promise + if (!isThenable(originalPromiseLike)) { + return instrumentedPromise; + } + + // Create a proxy that forwards Promise methods to instrumentedPromise + // and preserves additional methods from the original result + return new Proxy(originalPromiseLike, { + get(target: object, prop: string | symbol): unknown { + // For standard Promise methods (.then, .catch, .finally, Symbol.toStringTag), + // use instrumentedPromise to preserve Sentry instrumentation. + // For custom methods (like .withResponse()), use the original target. + const useInstrumentedPromise = prop in Promise.prototype || prop === Symbol.toStringTag; + const source = useInstrumentedPromise ? instrumentedPromise : target; + + const value = Reflect.get(source, prop) as unknown; + + // Special handling for .withResponse() to preserve instrumentation + // .withResponse() returns { data: T, response: Response, request_id: string } + if (prop === 'withResponse' && typeof value === 'function') { + return function wrappedWithResponse(this: unknown): unknown { + const originalWithResponse = (value as (...args: unknown[]) => unknown).call(target); + return createWithResponseWrapper(originalWithResponse, instrumentedPromise, mechanismType); + }; + } + + return typeof value === 'function' ? value.bind(source) : value; + }, + }) as Promise; +} diff --git a/packages/server-utils/src/async-context.ts b/packages/server-utils/src/async-context.ts index 1a12ba1a898a..ba7e0009c167 100644 --- a/packages/server-utils/src/async-context.ts +++ b/packages/server-utils/src/async-context.ts @@ -3,9 +3,12 @@ import type { Scope } from '@sentry/core'; import { _INTERNAL_createTracingChannelBinding, getAsyncContextStrategy, + _INTERNAL_safeMathRandom, + generateTraceId, getDefaultCurrentScope, getDefaultIsolationScope, getMainCarrier, + isContinuingTrace, setAsyncContextStrategy, } from '@sentry/core'; @@ -64,6 +67,19 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void { const scope = getScopes().scope.clone(); const isolationScope = getScopes().isolationScope.clone(); + // When forking an isolation scope, unless we are continuing an incoming + // trace, we give the freshly forked scope its own trace. This way, new + // root spans in an isolation scope will get separate traces. The previous + // trace's `sampled` and `propagationSpanId` are dropped on purpose. + // Carrying them over would apply the old trace's sampling decision to the + // new one and propagate a span id from a different trace. + if (!isContinuingTrace(scope.getPropagationContext())) { + scope.setPropagationContext({ + traceId: generateTraceId(), + sampleRand: _INTERNAL_safeMathRandom(), + }); + } + return asyncStorage.run({ scope, isolationScope }, () => { return callback(isolationScope); }); diff --git a/packages/server-utils/test/async-context.test.ts b/packages/server-utils/test/async-context.test.ts index 3e0566d58f06..364c7a1fd234 100644 --- a/packages/server-utils/test/async-context.test.ts +++ b/packages/server-utils/test/async-context.test.ts @@ -213,6 +213,77 @@ describe('withIsolationScope()', () => { expect(initialScope.getScopeData().tags).toEqual({ aa: 'aa' }); })); + + it('gives each forked isolation scope its own trace id when not continuing an incoming trace', () => { + const traceIds: string[] = []; + + withIsolationScope(() => { + traceIds.push(getCurrentScope().getPropagationContext().traceId); + }); + withIsolationScope(() => { + traceIds.push(getCurrentScope().getPropagationContext().traceId); + }); + + expect(traceIds[0]).toMatch(/^[a-f0-9]{32}$/); + expect(traceIds[1]).toMatch(/^[a-f0-9]{32}$/); + expect(traceIds[0]).not.toBe(traceIds[1]); + }); + + it('keeps the trace id when continuing an incoming trace (parentSpanId set)', () => { + const incomingTraceId = 'cafecafecafecafecafecafecafecafe'; + getCurrentScope().setPropagationContext({ + traceId: incomingTraceId, + parentSpanId: '1234567890abcdef', + sampleRand: 0.42, + }); + + withIsolationScope(() => { + expect(getCurrentScope().getPropagationContext().traceId).toBe(incomingTraceId); + }); + }); + + // A trace-id-only `sentry-trace` header yields a propagation context with a `dsc` but no + // `parentSpanId`, because the span id is optional in the header. Such a trace is still being + // continued, so its trace id must survive the fork. + it('keeps the trace id when continuing an incoming trace without a span id (dsc set)', () => { + const incomingTraceId = 'cafecafecafecafecafecafecafecafe'; + getCurrentScope().setPropagationContext({ + traceId: incomingTraceId, + sampleRand: 0.42, + dsc: { trace_id: incomingTraceId, sample_rate: '1' }, + }); + + withIsolationScope(() => { + const propagationContext = getCurrentScope().getPropagationContext(); + + expect(propagationContext.traceId).toBe(incomingTraceId); + expect(propagationContext.dsc).toEqual({ trace_id: incomingTraceId, sample_rate: '1' }); + }); + }); + + // A new trace must not inherit the previous trace's sampling decision or propagation span id. + // Keeping them would apply the old trace's sampling decision to the new one and propagate a + // span id belonging to a different trace. + it('drops the previous trace data when giving a forked isolation scope its own trace', () => { + const oldTraceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + getCurrentScope().setPropagationContext({ + traceId: oldTraceId, + sampleRand: 0.1, + sampled: true, + propagationSpanId: 'bbbbbbbbbbbbbbbb', + }); + + withIsolationScope(() => { + const propagationContext = getCurrentScope().getPropagationContext(); + + expect(propagationContext.traceId).toMatch(/^[a-f0-9]{32}$/); + expect(propagationContext.traceId).not.toBe(oldTraceId); + expect(propagationContext.sampleRand).toEqual(expect.any(Number)); + expect(propagationContext.sampled).toBeUndefined(); + expect(propagationContext.propagationSpanId).toBeUndefined(); + expect(propagationContext.dsc).toBeUndefined(); + }); + }); }); describe('AsyncLocalStorage re-use', () => { diff --git a/packages/vercel-edge/test/middlewareTraceIsolation.test.ts b/packages/vercel-edge/test/middlewareTraceIsolation.test.ts index 503c692a7a8d..0338de190a0a 100644 --- a/packages/vercel-edge/test/middlewareTraceIsolation.test.ts +++ b/packages/vercel-edge/test/middlewareTraceIsolation.test.ts @@ -1,5 +1,12 @@ import { context, propagation, ROOT_CONTEXT, trace } from '@opentelemetry/api'; -import { getCurrentScope, getGlobalScope, getIsolationScope, GLOBAL_OBJ, spanToJSON } from '@sentry/core'; +import { + getCurrentScope, + getGlobalScope, + getIsolationScope, + GLOBAL_OBJ, + spanToJSON, + withIsolationScope, +} from '@sentry/core'; import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; import { AsyncLocalStorage } from 'async_hooks'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; @@ -59,24 +66,28 @@ async function runMiddlewareRequest( delay = 0, ): Promise<{ traceId: string; childTraceId: string }> { const request = new Request(url, { method: 'GET', headers }); - return withPropagatedContext(request.headers, () => - nextMiddlewareTrace(new URL(url).pathname, async () => { - const rootSpan = trace.getActiveSpan()!; - const traceId = spanToJSON(rootSpan).trace_id!; - - await new Promise(resolve => setTimeout(resolve, delay)); - - // A child span started within the request must parent onto this request's root span (same trace id), not onto a - // concurrent request's span. - const childTracer = trace.getTracer('user'); - const childSpan = childTracer.startSpan('user-work'); - const childTraceId = spanToJSON(childSpan as unknown as Parameters[0]).trace_id!; - childSpan.end(); - - await new Promise(resolve => setTimeout(resolve, delay)); - - return { traceId, childTraceId }; - }), + // Mirror `wrapMiddlewareWithSentry`: fork an isolation scope per request, then run Next's + // `withPropagatedContext` + `Middleware.execute` trace inside it. + return withIsolationScope(() => + withPropagatedContext(request.headers, () => + nextMiddlewareTrace(new URL(url).pathname, async () => { + const rootSpan = trace.getActiveSpan()!; + const traceId = spanToJSON(rootSpan).trace_id!; + + await new Promise(resolve => setTimeout(resolve, delay)); + + // A child span started within the request must parent onto this request's root span (same trace id), not onto a + // concurrent request's span. + const childTracer = trace.getTracer('user'); + const childSpan = childTracer.startSpan('user-work'); + const childTraceId = spanToJSON(childSpan as unknown as Parameters[0]).trace_id!; + childSpan.end(); + + await new Promise(resolve => setTimeout(resolve, delay)); + + return { traceId, childTraceId }; + }), + ), ); } diff --git a/yarn.lock b/yarn.lock index cef60fa7bf25..895c568f6d88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8742,11 +8742,6 @@ dependencies: "@types/node" "*" -"@types/cookie@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" - integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== - "@types/cookie@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" @@ -9512,7 +9507,7 @@ "@types/node" "*" "@types/webidl-conversions" "*" -"@types/ws@*", "@types/ws@^8.18.1", "@types/ws@^8.5.1", "@types/ws@^8.5.10": +"@types/ws@*", "@types/ws@^8.18.1", "@types/ws@^8.5.1", "@types/ws@^8.5.10", "@types/ws@^8.5.12": version "8.18.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== @@ -14043,7 +14038,7 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~4.3.1, debug@~4.3.4: +debug@~4.3.4: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -15307,20 +15302,20 @@ engine.io-parser@~5.2.1: integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== engine.io@~6.6.0: - version "6.6.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.2.tgz#32bd845b4db708f8c774a4edef4e5c8a98b3da72" - integrity sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw== + version "6.6.9" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.9.tgz#fd17e9f4e3a256423592574b60ac262e91af4b82" + integrity sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg== dependencies: - "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" "@types/node" ">=10.0.0" + "@types/ws" "^8.5.12" accepts "~1.3.4" base64id "2.0.0" cookie "~0.7.2" cors "~2.8.5" - debug "~4.3.1" + debug "~4.4.1" engine.io-parser "~5.2.1" - ws "~8.17.1" + ws "~8.21.0" enhanced-resolve@^5.10.0, enhanced-resolve@^5.14.1, enhanced-resolve@^5.17.4, enhanced-resolve@^5.18.0, enhanced-resolve@^5.22.0: version "5.24.0" @@ -27051,9 +27046,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3, shell-quote@^1.8.4: - version "1.8.4" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190" - integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ== + version "1.10.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.10.0.tgz#482033e192e4f5c07151521ffa03400ec71b1b0f" + integrity sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA== shellwords@^0.1.1: version "0.1.1" @@ -30314,9 +30309,9 @@ webpack@^5.0.0, webpack@^5.106.1, webpack@^5.95.0: webpack-sources "^3.5.0" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + version "0.7.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.5.tgz#569d22764ab21f2de20af0e74b411e8ae5a0fa46" + integrity sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA== dependencies: http-parser-js ">=0.5.1" safe-buffer ">=5.1.0" @@ -30714,10 +30709,10 @@ ws@8.18.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@^8.13.0, ws@^8.18.0, ws@^8.18.3, ws@^8.20.1, ws@^8.4.2: - version "8.20.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.1.tgz#91a9ae2b312ccf98e0a85ec499b48cef45ab0ddb" - integrity sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w== +ws@^8.13.0, ws@^8.18.0, ws@^8.18.3, ws@^8.20.1, ws@^8.4.2, ws@~8.21.0: + version "8.21.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586" + integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw== ws@~8.17.1: version "8.17.1"