From e10598c875b604a59a8aeaa6bb8cf3e09e818a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Mon, 5 Oct 2026 10:13:35 +0300 Subject: [PATCH 01/73] test(e2e): Add Bun, Deno and Cloudflare variants to react-router-8-framework (#24598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POC for running one framework e2e app on several server runtimes instead of one app per runtime (Linear project P-JS-2537). `react-router-8-framework` now runs its full Playwright suite on Bun, Deno and Cloudflare (local workerd) as optional variants, next to the Node job. Each runtime inits its own SDK, the way a user of that runtime would: Node `@sentry/react-router`, Bun `@sentry/bun`, Deno `@sentry/deno` (with `--preload=@sentry/deno/import`) and Cloudflare `@sentry/cloudflare`. `@sentry/react-router` then only provides the framework wrappers, so values from its `init()` (`sdk.name`, the `runtime` tag, the `/__manifest` filter) are Node-only, and the tests branch on that. Using the runtime SDKs is what makes the Bun variant work: `@sentry/node` creates no `http.server` span on Bun, `@sentry/bun` does. A variant declares its runtime with a `runtime` field instead of the label. From it, both e2e jobs install Bun or Deno, the runner and CI set `RUNTIME` for the build and the assert command (read with the new `getRuntime()` from `@sentry-internal/test-utils`, and `playwright.config.mjs` picks the start command from it), and the runner copies the files named for that runtime over their base files in the temporary copy of the app. So the Cloudflare build gets `vite.cloudflare.config.ts` (`@cloudflare/vite-plugin` + `sentryCloudflareVitePlugin`) and `entry.server.cloudflare.tsx`, while Bun and Deno reuse the Node build. React Router has no option to pick a server entry, and this way a variant needs no framework or bundler config and no `build.yml` change. The Bun and Deno variants of `hono-4` and `hono-4-legacy` set `runtime` too, because CI no longer reads the runtime from the label. The convention is in the e2e README. Other expected differences: Express is not instrumented under `bun run` and there is no Express layer on workerd, so there the error transaction stays the request path and the meta tag names the `http.server` span. Deno is pinned to v2.9.0, because Deno 2.8 loses async context in socket callbacks and drops `ioredis` spans. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- .github/workflows/build.yml | 17 ++++-- dev-packages/e2e-tests/README.md | 54 +++++++++++++++++++ dev-packages/e2e-tests/ciCopyToTemp.ts | 5 ++ dev-packages/e2e-tests/lib/runtimeFiles.ts | 33 ++++++++++++ dev-packages/e2e-tests/run.ts | 15 +++++- .../hono-4-legacy/package.json | 2 + .../test-applications/hono-4/package.json | 2 + .../app/entry.server.cloudflare.tsx | 50 +++++++++++++++++ .../app/routes/performance/redis.tsx | 4 +- .../routes/performance/with-middleware.tsx | 6 +-- .../react-router-8-framework/deno.json | 6 +++ .../instrument.bun.mjs | 8 +++ .../instrument.deno.mjs | 8 +++ .../react-router-8-framework/package.json | 25 ++++++++- .../playwright.config.mjs | 11 +++- .../tests/constants.ts | 4 ++ .../tests/errors/errors.server.test.ts | 31 +++++++---- .../low-quality-filter.server.test.ts | 7 ++- .../performance/navigation.client.test.ts | 6 +-- .../tests/performance/pageload.client.test.ts | 3 +- .../performance/performance.server.test.ts | 12 +++-- .../performance/trace-propagation.test.ts | 9 +++- .../vite.cloudflare.config.ts | 19 +++++++ .../react-router-8-framework/workers/app.ts | 9 ++++ .../workers/instrument.server.ts | 8 +++ .../react-router-8-framework/wrangler.jsonc | 10 ++++ dev-packages/test-utils/src/index.ts | 2 + dev-packages/test-utils/src/runtime.ts | 17 ++++++ 28 files changed, 348 insertions(+), 35 deletions(-) create mode 100644 dev-packages/e2e-tests/lib/runtimeFiles.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc create mode 100644 dev-packages/test-utils/src/runtime.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f680c6c3c630..a37d3618311f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1038,6 +1038,8 @@ jobs: REACT_APP_E2E_TEST_DSN: 'https://username@domain/123' E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks' E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests' + # The `runtime` of a runtime variant, see dev-packages/e2e-tests/README.md + RUNTIME: ${{ matrix.runtime }} strategy: fail-fast: false matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix) }} @@ -1056,7 +1058,7 @@ jobs: - name: Set up Bun if: matrix.test-application == 'node-exports-test-app' || contains(matrix.test-application, 'bun') || - contains(matrix.label, 'bun') + matrix.runtime == 'bun' uses: oven-sh/setup-bun@v2 with: bun-version: '1.3.14' @@ -1067,7 +1069,7 @@ jobs: use-installer: true token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Deno - if: contains(matrix.test-application, 'deno') || contains(matrix.label, 'deno') + if: contains(matrix.test-application, 'deno') || matrix.runtime == 'deno' uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} @@ -1177,6 +1179,8 @@ jobs: # Used by test apps that deploy a real Cloudflare Worker, e.g. cloudflare-workers-send-to-sentry CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # The `runtime` of a runtime variant, see dev-packages/e2e-tests/README.md + RUNTIME: ${{ matrix.runtime }} strategy: fail-fast: false matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix-optional) }} @@ -1193,8 +1197,15 @@ jobs: uses: actions/setup-node@v7 with: node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' + - name: Set up Bun + if: + matrix.test-application == 'node-exports-test-app' || contains(matrix.test-application, 'bun') || + matrix.runtime == 'bun' + uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.14' - name: Set up Deno - if: matrix.test-application == 'deno' + if: contains(matrix.test-application, 'deno') || matrix.runtime == 'deno' uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index cb2964332492..e6424f4fa55f 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -92,6 +92,8 @@ For example, if you have the following variants in your test app's `package.json If you run `yarn test:run nextjs-pages-dir --variant 13`, it will match against the very first matching variant, which is `nextjs-pages-dir (next@13)`. If you need to target the second variant in the example, you need to be more specific and use `--variant 13-canary`. +A variant runs its `build-command` and `assert-command`. Without them, it runs the app's `test:build` and `test:assert` scripts, like the app's default job. + ## How they work We build our packages, pack them into tarballs (`yarn build:tarball`), and create symlinks in the `packed/` directory @@ -134,6 +136,58 @@ Apps like `cloudflare-workers-send-to-sentry` deploy the built app as a real Wor - Set `"sentryTest": { "optional": true }` in `package.json`, because only the optional CI job has the Cloudflare secrets. +## Runtime variants (Bun, Deno, Cloudflare) + +To test a framework on a runtime other than Node.js, add an `optionalVariants` entry to the existing test app instead of +creating a new app. `react-router-8-framework` is the reference setup. + +- **`runtime`**: set `"runtime": "bun"`, `"deno"` or `"cloudflare"` on the variant. For such a variant, CI installs Bun + or Deno, and the runner and CI set the `RUNTIME` env var for the build and the assert command. Without `runtime`, + `RUNTIME` is empty or not set, which means `node`. So a runtime variant can use the default `test:build` and + `test:assert` scripts, with no `test:assert:` script, and a new variant needs no change to + `.github/workflows/build.yml`. +- **`RUNTIME` env var**: read it with `getRuntime()` from `@sentry-internal/test-utils`, which throws on any value other + than `node`, `bun`, `deno` or `cloudflare`. Tests branch on it where the runtimes are expected to differ (for example + `platform` or `sdk.name`). +- **SDK per runtime**: each runtime inits its own SDK, the way a user of that runtime would: Node the framework SDK + (`instrument.mjs`), Bun `@sentry/bun` (`instrument.bun.mjs`), Deno `@sentry/deno` (`instrument.deno.mjs`) and + Cloudflare `@sentry/cloudflare`. The framework SDK only provides the framework wrappers on the other runtimes, so + values that come from its `init()` (`sdk.name`, default integrations) are Node-only. +- **Start commands**: `playwright.config.mjs` selects the start command with `getRuntime()`. Bun and Deno use the same build + as Node and only change the start command, for example + `bun --bun --preload ./instrument.bun.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js` and + `deno run -A --preload=@sentry/deno/import --preload=./instrument.deno.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js`. + The app's `deno.json` maps `@sentry/deno/import` to `node_modules`, because the e2e dependencies are `file:` tarballs + that `npm:@sentry/deno/import` does not resolve. +- **Cloudflare**: the app has the Cloudflare dependencies installed all the time. The Cloudflare build has its own + `vite.cloudflare.config.ts` with `@cloudflare/vite-plugin` and `sentryCloudflareVitePlugin` from + `@sentry/cloudflare/vite`, which the runner uses in place of `vite.config.ts` (see runtime-specific files). List the + Sentry plugins after all other plugins: they set no `enforce`, so their position decides when their hooks run, and a + sourcemap upload has to see the final output. Node, Bun and Deno share `vite.config.ts`. The Worker entry lives in a + separate file (for example `workers/app.ts`) and exports a plain handler: the Sentry plugin wraps it with + `withSentry` and reads the init options from `instrument.server.ts` next to the entry. The start command runs + `wrangler dev` on the build output. Code at module scope must not do I/O (for example open a database connection), + because workerd does not allow it. +- **Runtime-specific files**: a file that has the runtime as a part of its name replaces the existing file without that + part, for example `app/entry.server.cloudflare.tsx` replaces `app/entry.server.tsx` and `vite.cloudflare.config.ts` + replaces `vite.config.ts`. For a variant with `runtime`, the runner copies these files over the others in the + temporary copy of the app, before the build. This needs no framework or bundler config. A build in the app folder + itself does not get these files, so run a variant with `yarn test:run --variant `. +- **Deno version**: a variant can pin the Deno version, for example `"deno-version": "v2.9.0"`. +- **Bun**: under `bun run` the SDK cannot inject diagnostics channels into packages that stay outside the build (for + example Express behind `react-router-serve`), so those produce no spans on Bun. Where a test depends on them, branch + on `RUNTIME` and say why in a comment. + +```json +"sentryTest": { + "optionalVariants": [ + { "runtime": "bun", "label": "my-app (bun)" }, + { "runtime": "deno", "label": "my-app (deno)" }, + { "runtime": "cloudflare", "label": "my-app (cloudflare)" } + ] +} +``` + ## Troubleshooting ### Common Issues diff --git a/dev-packages/e2e-tests/ciCopyToTemp.ts b/dev-packages/e2e-tests/ciCopyToTemp.ts index 24c3629d8dce..649fe01fed3f 100644 --- a/dev-packages/e2e-tests/ciCopyToTemp.ts +++ b/dev-packages/e2e-tests/ciCopyToTemp.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { copyToTemp } from './lib/copyToTemp'; +import { applyRuntimeFiles } from './lib/runtimeFiles'; async function run(): Promise { const originalPath = process.argv[2]; @@ -13,6 +14,10 @@ async function run(): Promise { console.log(`Copying ${originalPath} to ${tmpDirPath}...`); await copyToTemp(originalPath, tmpDirPath); + + if (process.env.RUNTIME) { + applyRuntimeFiles(tmpDirPath, process.env.RUNTIME); + } } run().catch(error => { diff --git a/dev-packages/e2e-tests/lib/runtimeFiles.ts b/dev-packages/e2e-tests/lib/runtimeFiles.ts new file mode 100644 index 000000000000..a29c0bc890d3 --- /dev/null +++ b/dev-packages/e2e-tests/lib/runtimeFiles.ts @@ -0,0 +1,33 @@ +/* eslint-disable no-console */ +import { copyFileSync, existsSync, readdirSync } from 'fs'; +import { join } from 'path'; + +/** + * Copies each file in `dir` that has `runtime` as a part of its name over the existing file without that part, for + * example `app/entry.server.cloudflare.tsx` over `app/entry.server.tsx`. + */ +export function applyRuntimeFiles(dir: string, runtime: string): void { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) { + continue; + } + + const path = join(dir, entry.name); + if (entry.isDirectory()) { + applyRuntimeFiles(path, runtime); + continue; + } + + const segments = entry.name.split('.'); + const index = segments.indexOf(runtime); + if (index <= 0 || index === segments.length - 1) { + continue; + } + + const target = join(dir, segments.filter((_, i) => i !== index).join('.')); + if (existsSync(target)) { + copyFileSync(path, target); + console.log(`Copied ${path} to ${target} for ${runtime}`); + } + } +} diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 317aa42a3a64..f3a7f115925e 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -6,6 +6,7 @@ import { sync as globSync } from 'glob'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; import { copyToTemp } from './lib/copyToTemp'; +import { applyRuntimeFiles } from './lib/runtimeFiles'; import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks'; import { addPnpmOverrides } from './lib/pnpmOverrides'; @@ -14,6 +15,8 @@ interface SentryTestVariant { 'assert-command'?: string; label?: string; skip?: boolean; + /** Runtime of the variant (`bun`, `deno` or `cloudflare`). Sets `RUNTIME` and selects the runtime-specific files. */ + runtime?: string; } interface PackageJson { @@ -89,6 +92,7 @@ async function getVariantBuildCommand( testLabel: string; matchedVariantLabel?: string; skip?: boolean; + runtime?: string; }> { try { const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); @@ -108,6 +112,7 @@ async function getVariantBuildCommand( testLabel: matchingVariant.label || testAppPath, matchedVariantLabel: matchingVariant.label, skip: matchingVariant.skip, + runtime: matchingVariant.runtime, }; } @@ -232,16 +237,16 @@ async function run(): Promise { // at all - breaks the build with `unable to open database file`. Give each app its own. const sentryConfigDir = join(tmpDirPath, '.tmp_sentry_home'); await mkdir(sentryConfigDir, { recursive: true }); - const appEnv = { ...env, SENTRY_CONFIG_DIR: sentryConfigDir }; const cwd = tmpDirPath; // Resolve variant if needed - const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel + const { buildCommand, assertCommand, testLabel, matchedVariantLabel, runtime } = variantLabel ? await getVariantBuildCommand(join(tmpDirPath, 'package.json'), variantLabel, testAppPath) : { buildCommand: 'pnpm test:build', assertCommand: 'pnpm test:assert', testLabel: testAppPath, + runtime: undefined, }; // Print which variant we're using if found @@ -249,6 +254,12 @@ async function run(): Promise { console.log(`\n\nUsing variant: "${matchedVariantLabel}"\n\n`); } + if (runtime) { + applyRuntimeFiles(tmpDirPath, runtime); + } + + const appEnv = { ...env, SENTRY_CONFIG_DIR: sentryConfigDir, ...(runtime ? { RUNTIME: runtime } : {}) }; + console.log(`Building ${testLabel} in ${tmpDirPath}...`); await asyncExec(`volta run ${buildCommand}`, { env: appEnv, cwd }); diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json index ab3b68ce1f5d..2c0d72b3b1d6 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json @@ -40,10 +40,12 @@ "label": "hono-4-legacy (node)" }, { + "runtime": "bun", "assert-command": "RUNTIME=bun pnpm test:assert", "label": "hono-4-legacy (bun)" }, { + "runtime": "deno", "assert-command": "RUNTIME=deno pnpm test:assert", "label": "hono-4-legacy (deno)" } diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json index a3fd78b9ec67..99b2e1ce111e 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json @@ -41,10 +41,12 @@ "sentryTest": { "variants": [ { + "runtime": "bun", "assert-command": "pnpm test:assert:bun", "label": "hono-4 (bun)" }, { + "runtime": "deno", "assert-command": "pnpm test:assert:deno", "label": "hono-4 (deno)" }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx new file mode 100644 index 000000000000..e04fe621e718 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx @@ -0,0 +1,50 @@ +import { captureException } from '@sentry/cloudflare'; +import * as Sentry from '@sentry/react-router/cloudflare'; +import { isbot } from 'isbot'; +import { renderToReadableStream } from 'react-dom/server'; +import { type EntryContext, type HandleErrorFunction, ServerRouter } from 'react-router'; + +async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, +): Promise { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16)))); + + const body = await renderToReadableStream(, { + nonce, + signal: request.signal, + onError(error: unknown) { + responseStatusCode = 500; + // Errors thrown after the shell has flushed can't change the status code, so surface them. + if (shellRendered) { + // eslint-disable-next-line no-console + console.error(error); + } + }, + }); + shellRendered = true; + + if (userAgent && isbot(userAgent)) { + await body.allReady; + } + + responseHeaders.set('Content-Type', 'text/html'); + + return new Response(Sentry.injectTraceMetaTags(body), { + headers: responseHeaders, + status: responseStatusCode, + }); +} + +export const handleError: HandleErrorFunction = (error, { request }) => { + if (!request.signal.aborted) { + captureException(error, { mechanism: { type: 'react-router', handled: false } }); + console.error(error); + } +}; + +export default Sentry.wrapSentryHandleRequest(handleRequest); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx index cba8275fcf63..cf3eb5dbd4be 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/redis.tsx @@ -1,9 +1,11 @@ import Redis from 'ioredis'; import type { Route } from './+types/redis'; -const redis = new Redis(); +// workerd does not allow a socket connect at module scope, so the client is made in the loader. +let redis: Redis | undefined; export async function loader() { + redis ??= new Redis(); const key = 'cache:greeting'; await redis.set(key, 'hello from react-router'); const value = await redis.get(key); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx index c86f78e17164..0c8f45eee61d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/with-middleware.tsx @@ -11,11 +11,11 @@ async function getUser() { }; } -const authMiddleware: Route.MiddlewareFunction = async ({ request, context }, next) => { - Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => { +const authMiddleware: Route.MiddlewareFunction = async ({ context }, next) => { + return Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => { const user: User = await getUser(); context.set(userContext, user); - await next(); + return next(); }); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json new file mode 100644 index 000000000000..46456a69617a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/deno.json @@ -0,0 +1,6 @@ +{ + "imports": { + "@sentry/deno/import": "./node_modules/@sentry/deno/build/import.mjs" + }, + "nodeModulesDir": "manual" +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs new file mode 100644 index 000000000000..c2796e9f5278 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.bun.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/bun'; + +Sentry.init({ + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs new file mode 100644 index 000000000000..2dda332a88b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.deno.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/deno'; + +Sentry.init({ + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json index d4d993061d6f..a89e3ef6bc7f 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json @@ -9,11 +9,15 @@ "react-router": "^8.0.0", "@react-router/node": "^8.0.0", "@react-router/serve": "^8.0.0", + "@sentry/bun": "file:../../packed/sentry-bun-packed.tgz", + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "ioredis": "^5.11.1", "isbot": "^5.1.43" }, "devDependencies": { + "@cloudflare/vite-plugin": "^1.35.0", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/node": "^22", @@ -21,13 +25,17 @@ "@playwright/test": "~1.63.0", "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.6.3", - "vite": "^7.3.2" + "vite": "^7.3.2", + "wrangler": "^4.120.0" }, "scripts": { "build": "react-router build", "test:build-latest": "pnpm install && pnpm add react-router@latest && pnpm add @react-router/node@latest && pnpm add @react-router/serve@latest && pnpm build", "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev", "start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js", + "start:bun": "NODE_ENV=production bun --bun --preload ./instrument.bun.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js", + "start:deno": "NODE_ENV=production deno run -A --preload=@sentry/deno/import --preload=./instrument.deno.mjs ./node_modules/@react-router/serve/bin.cjs ./build/server/index.js", + "start:cloudflare": "wrangler dev --config ./build/server/wrangler.json --port 3030", "proxy": "node start-event-proxy.mjs", "typecheck": "react-router typegen && tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", @@ -64,6 +72,21 @@ "build-command": "pnpm test:build-latest", "label": "react-router-8-framework (latest)" } + ], + "optionalVariants": [ + { + "runtime": "bun", + "label": "react-router-8-framework (bun)" + }, + { + "runtime": "deno", + "deno-version": "v2.9.0", + "label": "react-router-8-framework (deno)" + }, + { + "runtime": "cloudflare", + "label": "react-router-8-framework (cloudflare)" + } ] } } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs index 70e62c2b9e3b..6e93d6fc020e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/playwright.config.mjs @@ -1,9 +1,16 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +import { getPlaywrightConfig, getRuntime } from '@sentry-internal/test-utils'; import { fileURLToPath } from 'url'; +const startCommands = { + node: 'PORT=3030 pnpm start', + bun: 'PORT=3030 pnpm start:bun', + deno: 'PORT=3030 pnpm start:deno', + cloudflare: 'pnpm start:cloudflare', +}; + const config = getPlaywrightConfig( { - startCommand: `PORT=3030 pnpm start`, + startCommand: startCommands[getRuntime()], port: 3030, }, // Boot Redis before the tests run, outside the webServer startup-timeout window. diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts index f6971b89941e..68883a5e51d4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/constants.ts @@ -1 +1,5 @@ +import { getRuntime } from '@sentry-internal/test-utils'; + +export const RUNTIME = getRuntime(); + export const APP_NAME = 'react-router-8-framework'; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts index acb11603fedc..bf436aa3c866 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('server-side errors', () => { test('captures error thrown in server loader', async ({ page }) => { @@ -26,21 +26,26 @@ test.describe('server-side errors', () => { }, ], }, - // todo: should be 'GET /errors/server-loader' - transaction: 'GET /{*splat}', + // Express names the transaction on Node and Deno. On Bun, where Express is not instrumented under + // `bun run`, it stays the request path. On Cloudflare the error has no transaction. + // todo: should be 'GET /errors/server-loader' everywhere + ...(RUNTIME === 'cloudflare' + ? {} + : { transaction: RUNTIME === 'bun' ? 'GET /errors/server-loader' : 'GET /{*splat}' }), request: { url: expect.stringContaining('errors/server-loader'), headers: expect.any(Object), }, level: 'error', - platform: 'node', + // Only Node inits `@sentry/react-router`. Bun, Deno and Cloudflare init their runtime's own SDK. + platform: RUNTIME === 'cloudflare' || RUNTIME === 'deno' ? 'javascript' : 'node', environment: 'qa', sdk: { integrations: expect.any(Array), - name: 'sentry.javascript.react-router', + name: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, version: expect.any(String), }, - tags: { runtime: 'node' }, + ...(RUNTIME === 'node' ? { tags: { runtime: 'node' } } : {}), contexts: { trace: { span_id: expect.any(String), @@ -74,21 +79,25 @@ test.describe('server-side errors', () => { }, ], }, - // todo: should be 'POST /errors/server-action' - transaction: 'POST /{*splat}', + // Express names the transaction on Node and Deno. On Bun, where Express is not instrumented under + // `bun run`, it stays the request path. On Cloudflare the error has no transaction. + // todo: should be 'POST /errors/server-action' everywhere + ...(RUNTIME === 'cloudflare' + ? {} + : { transaction: RUNTIME === 'bun' ? 'POST /errors/server-action.data' : 'POST /{*splat}' }), request: { url: expect.stringContaining('errors/server-action'), headers: expect.any(Object), }, level: 'error', - platform: 'node', + platform: RUNTIME === 'cloudflare' || RUNTIME === 'deno' ? 'javascript' : 'node', environment: 'qa', sdk: { integrations: expect.any(Array), - name: 'sentry.javascript.react-router', + name: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, version: expect.any(String), }, - tags: { runtime: 'node' }, + ...(RUNTIME === 'node' ? { tags: { runtime: 'node' } } : {}), contexts: { trace: { span_id: expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts index a1f44a541709..417c665f7314 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts @@ -1,10 +1,15 @@ import { expect, test } from '@playwright/test'; import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('low-quality span filter', () => { test('does not send a server span for /__manifest? requests', async ({ page }) => { + test.skip( + RUNTIME !== 'node', + "The filter is a default integration of `@sentry/react-router`'s `init()`, which only Node uses; the other runtimes init their own SDK", + ); + const streamedSpans: SerializedStreamedSpan[] = []; const navigationPromise = waitForStreamedSpan(APP_NAME, span => { diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts index e203c87b9664..419bc727d874 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts @@ -78,7 +78,7 @@ test.describe('client - navigation performance', () => { return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); - await page.goto(`/performance`); // pageload + await page.goto(`/performance/`); // pageload await pageloadSpanPromise; await page.getByRole('link', { name: 'Search Only Navigate' }).click(); // navigation with search-only object to @@ -88,7 +88,6 @@ test.describe('client - navigation performance', () => { 'sentry.op': { value: 'navigation', type: 'string' }, 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, 'url.template': { value: '/performance', type: 'string' }, - // the initial pageload to `/performance` gets 301-redirected to a trailing slash by react-router-serve 'url.path': { value: '/performance/', type: 'string' }, 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/\?query=test$/), @@ -143,7 +142,7 @@ test.describe('client - navigation performance', () => { return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); - await page.goto(`/performance`); + await page.goto(`/performance/`); await pageloadSpanPromise; const forwardNavPromise = waitForStreamedSpan(APP_NAME, span => { @@ -166,7 +165,6 @@ test.describe('client - navigation performance', () => { 'sentry.op': { value: 'navigation', type: 'string' }, 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, 'url.template': { value: '/performance', type: 'string' }, - // react-router-serve 301-redirects the bare index route to a trailing slash 'url.path': { value: '/performance/', type: 'string' }, 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts index 15ece1778f40..e7dfc682e436 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts @@ -8,7 +8,7 @@ test.describe('client - pageload performance', () => { return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); - await page.goto(`/performance`); + await page.goto(`/performance/`); const span = await spanPromise; @@ -30,7 +30,6 @@ test.describe('client - pageload performance', () => { 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, 'url.template': { value: '/performance', type: 'string' }, - // react-router-serve 301-redirects the bare index route to a trailing slash 'url.path': { value: '/performance/', type: 'string' }, 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts index 432b7c60f5f0..4488f1065229 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('server - performance', () => { test('should send server span on pageload', async ({ page }) => { @@ -26,7 +26,10 @@ test.describe('server - performance', () => { 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, 'sentry.segment.name.source': { value: 'route', type: 'string' }, 'sentry.environment': { value: 'qa', type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.name': { + value: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, + type: 'string', + }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, 'url.full': { value: expect.stringContaining('/performance'), type: 'string' }, @@ -56,7 +59,10 @@ test.describe('server - performance', () => { 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, 'sentry.segment.name.source': { value: 'route', type: 'string' }, 'sentry.environment': { value: 'qa', type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.name': { + value: RUNTIME === 'node' ? 'sentry.javascript.react-router' : `sentry.javascript.${RUNTIME}`, + type: 'string', + }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, 'url.full': { value: expect.stringContaining('/performance/with/some-param'), type: 'string' }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts index 9ac6ccb807b1..b2991b7f0dc0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; +import { APP_NAME, RUNTIME } from '../constants'; test.describe('Trace propagation', () => { test('should inject metatags in ssr pageload', async ({ page }) => { @@ -51,7 +51,12 @@ test.describe('Trace propagation', () => { const requestHandlerSpan = streamedSpans.find(span => span.span_id === handlerSpanId); expect(requestHandlerSpan).toBeDefined(); - expect(getSpanOp(requestHandlerSpan!)).toBe('handler'); + // On Node and Deno the Express request handler span is active while the page renders. Without an Express + // layer (Cloudflare, and Bun, where Express is not instrumented under `bun run`) the tag names the + // http.server segment itself. + expect(getSpanOp(requestHandlerSpan!)).toBe( + RUNTIME === 'cloudflare' || RUNTIME === 'bun' ? 'http.server' : 'handler', + ); expect(requestHandlerSpan!.trace_id).toBe(traceId); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts new file mode 100644 index 000000000000..8a905ba1e382 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.cloudflare.config.ts @@ -0,0 +1,19 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import { reactRouter } from '@react-router/dev/vite'; +import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite'; +import { sentryReactRouter } from '@sentry/react-router/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig(async config => ({ + plugins: [ + cloudflare({ viteEnvironment: { name: 'ssr' } }), + reactRouter(), + sentryCloudflareVitePlugin(), + ...((await sentryReactRouter( + // Both Sentry plugins inject the orchestrion snippet, and injecting it twice fails the build. + { sourcemaps: { disable: true }, buildTimeInstrumentation: false }, + config, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + )) as any[]), + ], +})); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts new file mode 100644 index 000000000000..e14713f82cde --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/app.ts @@ -0,0 +1,9 @@ +import { createRequestHandler } from 'react-router'; + +const requestHandler = createRequestHandler(() => import('virtual:react-router/server-build'), import.meta.env.MODE); + +export default { + async fetch(request: Request) { + return requestHandler(request); + }, +}; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts new file mode 100644 index 000000000000..be998ae29a13 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/workers/instrument.server.ts @@ -0,0 +1,8 @@ +import { defineCloudflareOptions } from '@sentry/cloudflare'; + +export default defineCloudflareOptions({ + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc new file mode 100644 index 000000000000..e9cb6bee7cd7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "react-router-8-framework", + "compatibility_date": "2026-06-29", + "compatibility_flags": ["nodejs_compat"], + "main": "./workers/app.ts", + "assets": { + "directory": "./build/client", + }, +} diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 215ec05693cb..e1b46f8a5818 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -32,6 +32,8 @@ export type { InstrumentationFixture } from './bundler-instrumentation'; export { hidePage } from './page'; export { getPlaywrightConfig } from './playwright-config'; +export { getRuntime } from './runtime'; +export type { Runtime } from './runtime'; export { createBasicSentryServer, createTestServer } from './server'; export { startMockSentryServer } from './mock-sentry-server'; diff --git a/dev-packages/test-utils/src/runtime.ts b/dev-packages/test-utils/src/runtime.ts new file mode 100644 index 000000000000..236948a7fc17 --- /dev/null +++ b/dev-packages/test-utils/src/runtime.ts @@ -0,0 +1,17 @@ +export type Runtime = 'node' | 'bun' | 'deno' | 'cloudflare'; + +const RUNTIMES: readonly Runtime[] = ['node', 'bun', 'deno', 'cloudflare']; + +/** + * Returns the server runtime a test app runs on, read from the `RUNTIME` env var that runtime variants + * set in their assert script. Defaults to `node`. + */ +export function getRuntime(): Runtime { + const runtime = process.env.RUNTIME || 'node'; + + if (!RUNTIMES.includes(runtime as Runtime)) { + throw new Error(`Unknown RUNTIME "${runtime}", expected one of: ${RUNTIMES.join(', ')}`); + } + + return runtime as Runtime; +} From b1264166654cbf4403512ea0919660e736463a19 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 09:44:25 +0200 Subject: [PATCH 02/73] test(node/bun): drop static pins from non-tracing integration suites (#25017) Remove static lifecycle pins from non-tracing integration suites so they exercise the default span-streaming lifecycle. Session and Pino tests now ignore span envelopes instead of transactions. Part of #24141 (JS-3611): mechanical pin-removal portion Co-authored-by: GPT-6 --- .../suites/bun-runtime-metrics/scenario-all.ts | 1 - .../suites/bun-runtime-metrics/scenario-opt-out.ts | 1 - .../suites/bun-runtime-metrics/scenario.ts | 1 - .../node-integration-tests/suites/anr/app-path.mjs | 1 - .../suites/anr/basic-multiple.mjs | 1 - .../suites/anr/basic-session.js | 1 - .../node-integration-tests/suites/anr/basic.js | 1 - .../node-integration-tests/suites/anr/basic.mjs | 1 - .../node-integration-tests/suites/anr/forked.js | 1 - .../suites/anr/indefinite.mjs | 1 - .../node-integration-tests/suites/anr/isolated.mjs | 1 - .../suites/anr/should-exit-forced.js | 1 - .../suites/anr/should-exit.js | 1 - .../suites/anr/stop-and-start.js | 1 - .../suites/breadcrumbs/process-thread/app.mjs | 1 - .../suites/child-process/fork.js | 1 - .../suites/child-process/fork.mjs | 1 - .../suites/child-process/worker.js | 1 - .../suites/child-process/worker.mjs | 1 - .../client-reports/periodic-send/scenario.ts | 1 - .../suites/consola/instrument.mjs | 1 - .../filename-with-spaces/instrument.mjs | 1 - .../suites/contextLines/memory-leak/scenario.ts | 1 - .../suites/cron/cron/scenario.ts | 1 - .../suites/cron/node-cron/base/scenario.ts | 1 - .../suites/cron/node-cron/isolateTrace/scenario.ts | 1 - .../suites/cron/node-schedule/scenario.ts | 1 - .../suites/esm/modules-integration/app.mjs | 1 - .../suites/esm/warn-esm/server.js | 1 - .../suites/esm/warn-esm/server.mjs | 1 - .../integrations/console/filter/instrument.mjs | 1 - .../suites/modules/instrument.mjs | 1 - .../suites/modules/server.js | 1 - .../suites/node-runtime-metrics/scenario-all.ts | 1 - .../node-runtime-metrics/scenario-opt-out.ts | 1 - .../suites/node-runtime-metrics/scenario.ts | 1 - .../suites/pino/instrument-auto-off.mjs | 1 - .../suites/pino/instrument.mjs | 1 - .../node-integration-tests/suites/pino/test.ts | 14 +++++++------- .../public-api/LocalVariables/deny-inspector.mjs | 2 +- .../LocalVariables/local-variables-caught.js | 1 - .../LocalVariables/local-variables-caught.mjs | 1 - .../LocalVariables/local-variables-disabled.js | 1 - .../LocalVariables/local-variables-filtered.js | 1 - .../LocalVariables/local-variables-instrument.cjs | 1 - .../local-variables-name-matching.js | 1 - .../local-variables-out-of-app-default.js | 1 - .../LocalVariables/local-variables-out-of-app.js | 1 - .../LocalVariables/local-variables-rethrow.js | 1 - .../public-api/LocalVariables/local-variables.js | 1 - .../LocalVariables/no-local-variables.js | 1 - .../additional-listener-test-script.js | 1 - .../suites/public-api/OnUncaughtException/basic.js | 1 - .../log-entire-error-to-console.js | 1 - ...ve-behaviour-additional-listener-test-script.js | 1 - ...behaviour-no-additional-listener-test-script.js | 1 - .../no-additional-listener-test-script.js | 1 - .../worker-thread/instrument.js | 1 - .../worker-thread/instrument.mjs | 1 - .../public-api/addBreadcrumb/empty-obj/scenario.ts | 1 - .../addBreadcrumb/multiple_breadcrumbs/scenario.ts | 1 - .../addBreadcrumb/simple_breadcrumb/scenario.ts | 1 - .../captureException/catched-error/scenario.ts | 1 - .../captureException/empty-obj/scenario.ts | 1 - .../captureException/simple-error/scenario.ts | 1 - .../parameterized_message/scenario.ts | 1 - .../captureMessage/simple_message/scenario.ts | 1 - .../simple_message_attachStackTrace/scenario.ts | 1 - .../captureMessage/with_level/scenario.ts | 1 - .../configureScope/set_properties/scenario.ts | 1 - .../suites/public-api/logger/scenario.ts | 1 - .../suites/public-api/metrics/scenario.ts | 1 - .../metrics/server-address-option/scenario.ts | 1 - .../public-api/metrics/server-address/scenario.ts | 1 - .../setContext/multiple-contexts/scenario.ts | 1 - .../non-serializable-context/scenario.ts | 1 - .../setContext/simple-context/scenario.ts | 1 - .../setExtra/multiple-extras/scenario.ts | 1 - .../setExtra/non-serializable-extra/scenario.ts | 1 - .../public-api/setExtra/simple-extra/scenario.ts | 1 - .../setExtras/consecutive-calls/scenario.ts | 1 - .../setExtras/multiple-extras/scenario.ts | 1 - .../public-api/setTag/with-primitives/scenario.ts | 1 - .../public-api/setTags/with-primitives/scenario.ts | 1 - .../public-api/setUser/unset_user/scenario.ts | 1 - .../public-api/setUser/update_user/scenario.ts | 1 - .../suites/public-api/withMonitor/scenario.ts | 1 - .../public-api/withScope/nested-scopes/scenario.ts | 1 - .../sessions/crashed-session-aggregate/test.ts | 2 +- .../sessions/errored-session-aggregate/test.ts | 2 +- .../sessions/exited-session-aggregate/test.ts | 2 +- .../suites/sessions/instrument.mjs | 1 - .../suites/system-error/basic-pii.mjs | 1 - .../suites/system-error/basic.mjs | 1 - .../suites/thread-blocked-native/app-path.mjs | 1 - .../thread-blocked-native/basic-disabled.mjs | 1 - .../thread-blocked-native/basic-multiple.mjs | 1 - .../suites/thread-blocked-native/basic.js | 1 - .../suites/thread-blocked-native/basic.mjs | 1 - .../suites/thread-blocked-native/indefinite.mjs | 1 - .../suites/thread-blocked-native/instrument.mjs | 1 - .../thread-blocked-native/should-exit-forced.js | 1 - .../suites/thread-blocked-native/should-exit.js | 1 - .../suites/vercel/sigterm-flush/scenario.ts | 1 - .../suites/winston/instrument.mjs | 1 - 105 files changed, 11 insertions(+), 111 deletions(-) diff --git a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts index 6fcc7d11fac5..e5acad073040 100644 --- a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts +++ b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-all.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/bun'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts index 47b9e22fafcd..5f481fe5838f 100644 --- a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts +++ b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/bun'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts index 7f4ee4d4dcf1..5fad87f86e5f 100644 --- a/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts +++ b/dev-packages/bun-integration-tests/suites/bun-runtime-metrics/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/bun'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-integration-tests/suites/anr/app-path.mjs index de06e125c084..8fcb638aeaae 100644 --- a/dev-packages/node-integration-tests/suites/anr/app-path.mjs +++ b/dev-packages/node-integration-tests/suites/anr/app-path.mjs @@ -12,7 +12,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs index 157a32cc4c94..71a0f21b18f5 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic-session.js b/dev-packages/node-integration-tests/suites/anr/basic-session.js index 8684deca9f02..70a814e0312c 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-session.js +++ b/dev-packages/node-integration-tests/suites/anr/basic-session.js @@ -6,7 +6,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic.js b/dev-packages/node-integration-tests/suites/anr/basic.js index e1e6adc31912..bb0ba6d54d3d 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.js +++ b/dev-packages/node-integration-tests/suites/anr/basic.js @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 4217c5d7c1a5..967a1ae7b132 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/forked.js b/dev-packages/node-integration-tests/suites/anr/forked.js index ca5f5f50c4a2..080ae1ed029d 100644 --- a/dev-packages/node-integration-tests/suites/anr/forked.js +++ b/dev-packages/node-integration-tests/suites/anr/forked.js @@ -6,7 +6,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/anr/indefinite.mjs b/dev-packages/node-integration-tests/suites/anr/indefinite.mjs index 6d8576a14a94..4b3b4c2787e7 100644 --- a/dev-packages/node-integration-tests/suites/anr/indefinite.mjs +++ b/dev-packages/node-integration-tests/suites/anr/indefinite.mjs @@ -6,7 +6,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/isolated.mjs b/dev-packages/node-integration-tests/suites/anr/isolated.mjs index 03883a0925ac..95452f723b93 100644 --- a/dev-packages/node-integration-tests/suites/anr/isolated.mjs +++ b/dev-packages/node-integration-tests/suites/anr/isolated.mjs @@ -5,7 +5,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], diff --git a/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js b/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js index bf37172abcc4..2536c48553e7 100644 --- a/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js +++ b/dev-packages/node-integration-tests/suites/anr/should-exit-forced.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/anr/should-exit.js b/dev-packages/node-integration-tests/suites/anr/should-exit.js index 7ecc3c140526..85ad4c508e17 100644 --- a/dev-packages/node-integration-tests/suites/anr/should-exit.js +++ b/dev-packages/node-integration-tests/suites/anr/should-exit.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js index e7359f552664..d1b2bc8d962c 100644 --- a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -7,7 +7,6 @@ setTimeout(() => { const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs index 31f242feae18..792d86199ecb 100644 --- a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs @@ -7,7 +7,6 @@ import { Worker } from 'worker_threads'; const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', integrations: [Sentry.childProcessIntegration(), Sentry.workerThreadsIntegration()], diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.js b/dev-packages/node-integration-tests/suites/child-process/fork.js index 2c9dcf0a3ae4..064a21d57657 100644 --- a/dev-packages/node-integration-tests/suites/child-process/fork.js +++ b/dev-packages/node-integration-tests/suites/child-process/fork.js @@ -4,7 +4,6 @@ const path = require('path'); const { fork } = require('child_process'); Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/child-process/fork.mjs b/dev-packages/node-integration-tests/suites/child-process/fork.mjs index 19d612141570..f28b7f313ec7 100644 --- a/dev-packages/node-integration-tests/suites/child-process/fork.mjs +++ b/dev-packages/node-integration-tests/suites/child-process/fork.mjs @@ -6,7 +6,6 @@ import * as path from 'path'; const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.js b/dev-packages/node-integration-tests/suites/child-process/worker.js index 56c574bb325e..5468f7f45a9d 100644 --- a/dev-packages/node-integration-tests/suites/child-process/worker.js +++ b/dev-packages/node-integration-tests/suites/child-process/worker.js @@ -4,7 +4,6 @@ const path = require('path'); const { Worker } = require('worker_threads'); Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/child-process/worker.mjs b/dev-packages/node-integration-tests/suites/child-process/worker.mjs index 5a8252e45554..d0a749d464e3 100644 --- a/dev-packages/node-integration-tests/suites/child-process/worker.mjs +++ b/dev-packages/node-integration-tests/suites/child-process/worker.mjs @@ -6,7 +6,6 @@ import { Worker } from 'worker_threads'; const __dirname = new URL('.', import.meta.url).pathname; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts b/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts index 9975b9feeb0b..72a46dcc5635 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/periodic-send/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, clientReportFlushInterval: 5000, diff --git a/dev-packages/node-integration-tests/suites/consola/instrument.mjs b/dev-packages/node-integration-tests/suites/consola/instrument.mjs index 4ce4f3c0ffa9..cededb10ce20 100644 --- a/dev-packages/node-integration-tests/suites/consola/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/consola/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs index 0418e09c174f..9ffde125d498 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts index d202b4d2e057..18df082e3b2d 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts @@ -4,7 +4,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts index f1e1ba0c3b8c..b3966b050d89 100644 --- a/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/cron/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { CronJob } from 'cron'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', }); diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts index bac06675d452..fb69a5de2482 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/base/scenario.ts @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as cron from 'node-cron'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts index 04aea6c84cae..b64b2d814b67 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-cron/isolateTrace/scenario.ts @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as cron from 'node-cron'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts index e905b9be4d6c..2bb67cc0047c 100644 --- a/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts +++ b/dev-packages/node-integration-tests/suites/cron/node-schedule/scenario.ts @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import * as schedule from 'node-schedule'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs b/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs index d45016438d03..a131c93e86fc 100644 --- a/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs +++ b/dev-packages/node-integration-tests/suites/esm/modules-integration/app.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', integrations: [Sentry.modulesIntegration()], diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js index 1bcb3b642b5a..0a2f192d9bc7 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs index a42337b2a47c..e7309895b992 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs @@ -3,7 +3,6 @@ import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry import express from 'express'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs index 7d9a68a1c677..16c05ae32ed0 100644 --- a/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/modules/instrument.mjs b/dev-packages/node-integration-tests/suites/modules/instrument.mjs index 432e6ae5475b..c8cdc9cac0a2 100644 --- a/dev-packages/node-integration-tests/suites/modules/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/modules/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/modules/server.js b/dev-packages/node-integration-tests/suites/modules/server.js index c57515443a5e..bf5ec659e060 100644 --- a/dev-packages/node-integration-tests/suites/modules/server.js +++ b/dev-packages/node-integration-tests/suites/modules/server.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts index f6a4e0a3162c..94a7161fe00c 100644 --- a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts +++ b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts index f56c4cd5fd2e..e838df6e9408 100644 --- a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts +++ b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts index b968e8c7a5c2..1e174e5c29cc 100644 --- a/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts +++ b/dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs b/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs index 5ea7061a8ffb..42c938885229 100644 --- a/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs +++ b/dev-packages/node-integration-tests/suites/pino/instrument-auto-off.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/pino/instrument.mjs b/dev-packages/node-integration-tests/suites/pino/instrument.mjs index b3975e128c04..009af91971f3 100644 --- a/dev-packages/node-integration-tests/suites/pino/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/pino/instrument.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/pino/test.ts b/dev-packages/node-integration-tests/suites/pino/test.ts index b5ef17cca2a2..d961b09c3200 100644 --- a/dev-packages/node-integration-tests/suites/pino/test.ts +++ b/dev-packages/node-integration-tests/suites/pino/test.ts @@ -11,7 +11,7 @@ describe('Pino integration', () => { await createRunner() .withMockSentryServer() .ignore('event') - .ignore('transaction') + .ignore('span') .expect({ log: log => { const traceId1 = log.items?.[0]?.trace_id; @@ -26,7 +26,7 @@ describe('Pino integration', () => { test('captures event and logs', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ event: { exception: { @@ -110,7 +110,7 @@ describe('Pino integration', () => { test('captures with Pino integrated channel', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ event: { exception: { @@ -193,7 +193,7 @@ describe('Pino integration', () => { test('captures logs when autoInstrument is false and logger is tracked', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ log: { items: [ @@ -246,7 +246,7 @@ describe('Pino integration', () => { test('captures structured logs with msg field', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ log: { items: [ @@ -308,7 +308,7 @@ describe('Pino integration', () => { test('attaches log message and fields to captured error events', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .ignore('log') .expect({ event: { @@ -356,7 +356,7 @@ describe('Pino integration', () => { test('captures logs with custom messageKey and errorKey', async () => { await createRunner() .withMockSentryServer() - .ignore('transaction') + .ignore('span') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs index 5fde6f9da29c..b63c0ecc4419 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs @@ -15,5 +15,5 @@ export async function resolve(specifier, context, nextResolve) { (async () => { const Sentry = await import('@sentry/node'); - Sentry.init({ traceLifecycle: 'static' }); + Sentry.init(); })(); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js index 822a315c08c8..8ea8dd93fb4d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs index f95449b27718..c0a26a6aa709 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js index 93d6b1ad15ca..e9695c1a4aae 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-disabled.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, dataCollection: { stackFrameVariables: false }, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js index 0574f218a884..48e6e36b83a3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-filtered.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, dataCollection: { stackFrameVariables: { deny: ['secretVar'] } }, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs index 1f998cbb4ddb..0b39efcfd8db 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.cjs @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js index 3487f3ae7007..b9027b749181 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-name-matching.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js index 6e400cce7eba..c8da0acc0d4e 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js @@ -12,7 +12,6 @@ function in_app_function() { } Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, includeLocalVariables: true, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js index 76fc88a8a905..119bbad71ccf 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js @@ -7,7 +7,6 @@ const externalFunctionFile = require.resolve('./node_modules/out-of-app-function const { out_of_app_function } = require(externalFunctionFile); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, includeLocalVariables: true, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js index 163520bb2f08..744cb747c70c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js index dd585e2846f3..8b64c80cbc5f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', includeLocalVariables: true, transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js index bcb24329cd20..2857fc0e3ce9 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js @@ -3,7 +3,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js index fc863f1eaeec..efb21fab10db 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js index d0aca741ba23..656a5b19220b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/basic.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js index 28362c14f05d..758f9e26cc5b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js @@ -1,7 +1,6 @@ const Sentry = require('@sentry/node'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js index fa2faf46d3cf..59af71f30c60 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.onUncaughtExceptionIntegration({ diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js index de44f866ef44..5eb71328de0e 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.onUncaughtExceptionIntegration({ diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js index c1c65df8cb84..9a8c69daaf35 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js index 75555e4a5952..a2b13b91bce6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { loggingTransport } = require('@sentry-internal/node-integration-tests'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, debug: false, diff --git a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs index 31831d03ff78..9263fe27bce1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/public-api/OnUncaughtException/worker-thread/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 0, debug: false, diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts index 6c4d5a448287..e49220c4d79d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts index c05db95cf7ad..2a28a7d19bb1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts index 5a30820da2cc..5d166004c87d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts index f9e2eea18d4d..53f994b1686e 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/catched-error/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts index 29da30447751..081af449fd81 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts index 749f52de5a63..f74a8eb68035 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureException/simple-error/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts index d225b20d9019..ed1a4bfb1c83 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts index 2eadd73a01d9..3d2375572830 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts index 73556ca773aa..4761a4afc1d3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts index d67046d55134..8ae145b43b71 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts index 405691623768..64b13cfd1db5 100644 --- a/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts index eac14291c9eb..74a43eb5356a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/logger/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts index bee87c236470..39817fc39b7d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts index b9d68b4f3d54..1f6d60f91cac 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address-option/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts index 80ee934d2820..a985f2a0fce3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/metrics/server-address/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts index c85e7f29ac5b..1922e8c69ef0 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts index 70c92c779b98..1b22d2d8e8ac 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts index 3d99cc5285d2..1fb504808b0f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setContext/simple-context/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts index 88ebfce95011..d063729bcc93 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts index 41d536441a13..6bbe78c88aa7 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts index 0efaf4267b84..4f66d74f7e7a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts index 2bb45f6cb085..2644bb47ea3c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts index 88e378a90019..f356949c6327 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts index 0e4ad0f07289..4e0bc30c0c2b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts index 0e4ad0f07289..4e0bc30c0c2b 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts index 8c6c3ba27de9..9e617168b539 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts index c9c548ace4a1..4c2eb2221bf6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts index ff3d57c57872..42a1da41a195 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withMonitor/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts index ef1d4b13c778..93f425ff6a89 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index b422aca7d289..8ab29a183bd2 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -9,7 +9,7 @@ afterAll(() => { test('should aggregate successful and crashed sessions', async () => { const runner = createRunner(__dirname, '..', 'server.mjs') .withInstrument(join(__dirname, '..', 'instrument.mjs')) - .ignore('transaction', 'event') + .ignore('span', 'event') .unignore('sessions') .expect({ sessions: agg => { diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index 8a3a91e6997a..9c9bd740af31 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -9,7 +9,7 @@ afterAll(() => { test('should aggregate successful, crashed and erroneous sessions', async () => { const runner = createRunner(__dirname, '..', 'server.mjs') .withInstrument(join(__dirname, '..', 'instrument.mjs')) - .ignore('transaction', 'event') + .ignore('span', 'event') .unignore('sessions') .expect({ sessions: agg => { diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index 79ebc5bf5566..173cfffee68c 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -9,7 +9,7 @@ afterAll(() => { test('should aggregate successful sessions', async () => { const runner = createRunner(__dirname, '..', 'server.mjs') .withInstrument(join(__dirname, '..', 'instrument.mjs')) - .ignore('transaction', 'event') + .ignore('span', 'event') .unignore('sessions') .expect({ sessions: agg => { diff --git a/dev-packages/node-integration-tests/suites/sessions/instrument.mjs b/dev-packages/node-integration-tests/suites/sessions/instrument.mjs index c8a21c59d4b2..e727f2c5068a 100644 --- a/dev-packages/node-integration-tests/suites/sessions/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/sessions/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs b/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs index 080e8a707c82..4bb7c6e1b4ee 100644 --- a/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs +++ b/dev-packages/node-integration-tests/suites/system-error/basic-pii.mjs @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import { readFileSync } from 'fs'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/system-error/basic.mjs b/dev-packages/node-integration-tests/suites/system-error/basic.mjs index 080e8a707c82..4bb7c6e1b4ee 100644 --- a/dev-packages/node-integration-tests/suites/system-error/basic.mjs +++ b/dev-packages/node-integration-tests/suites/system-error/basic.mjs @@ -3,7 +3,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; import { readFileSync } from 'fs'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs index 062a385c00b6..c561b221d95f 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs @@ -13,7 +13,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration({ appRootPath: __dirname })], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs index c90b4f630245..15a1f496de61 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-disabled.mjs @@ -9,7 +9,6 @@ setTimeout(() => { }, 15000); Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: process.env.SENTRY_DSN, release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs index bada88535eb4..32135d2246f2 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs @@ -9,7 +9,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration({ maxEventsPerHour: 2 })], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js index 1c17a01b5da7..30740bbd031b 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js @@ -9,7 +9,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration()], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs index 98a51621443d..49f30db2e59e 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs @@ -9,7 +9,6 @@ setTimeout(() => { }, 12000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration()], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs index ed0335e7f675..55eecb5c23ec 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs @@ -8,7 +8,6 @@ setTimeout(() => { }, 10000); Sentry.init({ - traceLifecycle: 'static', dsn: process.env.SENTRY_DSN, release: '1.0', integrations: [eventLoopBlockIntegration()], diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs index 4503cc90104b..ee66bf82f8bf 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { eventLoopBlockIntegration } from '@sentry/node-native'; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: process.env.SENTRY_DSN, release: '1.0', diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js index 467277f16d30..71622bdbe083 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js @@ -3,7 +3,6 @@ const { eventLoopBlockIntegration } = require('@sentry/node-native'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js index 426e4df15e02..cda4c0e10d3a 100644 --- a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js @@ -3,7 +3,6 @@ const { eventLoopBlockIntegration } = require('@sentry/node-native'); function configureSentry() { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, diff --git a/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts b/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts index 94466eb0986b..21aa0deb1a1e 100644 --- a/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts +++ b/dev-packages/node-integration-tests/suites/vercel/sigterm-flush/scenario.ts @@ -22,7 +22,6 @@ function bufferedLoggingTransport(_options: BaseTransportOptions): Transport { } Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: bufferedLoggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/winston/instrument.mjs b/dev-packages/node-integration-tests/suites/winston/instrument.mjs index 7ac52e645e47..f21bdc650450 100644 --- a/dev-packages/node-integration-tests/suites/winston/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/winston/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', From 5b3e480b7b45048230401e5b9702e6d104fb5db2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 10:00:34 +0200 Subject: [PATCH 03/73] test(bun): Port request-body tests to span streaming (#25021) Exercise Bun request-body capture with default span streaming, preserving the existing metadata, truncation, disabled-capture, and handler-body assertions. The body is available on the streamed segment as `http.request.body.data`, so this suite no longer needs a static lifecycle pin. Static transaction coverage remains in `elysia-bun-static`. Fixes #24134 --------- Co-authored-by: GPT-6 --- .../suites/request-bodies/index.ts | 2 - .../suites/request-bodies/test.ts | 37 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/dev-packages/bun-integration-tests/suites/request-bodies/index.ts b/dev-packages/bun-integration-tests/suites/request-bodies/index.ts index dbd9ecfd5dd2..fab2418ca66f 100644 --- a/dev-packages/bun-integration-tests/suites/request-bodies/index.ts +++ b/dev-packages/bun-integration-tests/suites/request-bodies/index.ts @@ -7,8 +7,6 @@ const mode = process.env.BODY_MODE; Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, - // Request bodies are only attached to transaction events, so this suite needs the static trace lifecycle. - traceLifecycle: 'static', ...(mode === 'explicit-small' && { dataCollection: { httpBodies: [] }, integrations: integrations => [ diff --git a/dev-packages/bun-integration-tests/suites/request-bodies/test.ts b/dev-packages/bun-integration-tests/suites/request-bodies/test.ts index 0134022d2b29..f81b2b99b619 100644 --- a/dev-packages/bun-integration-tests/suites/request-bodies/test.ts +++ b/dev-packages/bun-integration-tests/suites/request-bodies/test.ts @@ -9,13 +9,22 @@ test('captures incoming request bodies by default', async () => { const runner = createRunner(__dirname, 'index.ts') .withMockSentryServer() .expect({ - transaction: transaction => { - expect(transaction.request).toMatchObject({ - method: 'POST', - url: expect.stringContaining('/default'), - query_string: 'source=test', - headers: expect.objectContaining({ 'content-type': 'text/plain' }), - data: 'captured-by-default', + span: container => { + const span = container.items.find(span => span.is_segment); + expect(span).toBeDefined(); + expect(span?.attributes['http.request.method']).toEqual({ value: 'POST', type: 'string' }); + expect(span?.attributes['url.full']).toEqual({ + value: expect.stringContaining('/default'), + type: 'string', + }); + expect(span?.attributes['url.query']).toEqual({ value: 'source=test', type: 'string' }); + expect(span?.attributes['http.request.header.content-type']).toEqual({ + value: ['text/plain'], + type: 'array', + }); + expect(span?.attributes['http.request.body.data']).toEqual({ + value: 'captured-by-default', + type: 'string', }); }, }) @@ -34,8 +43,10 @@ test('an explicit small size overrides disabled body collection', async () => { .withMockSentryServer() .withEnv({ BODY_MODE: 'explicit-small' }) .expect({ - transaction: transaction => { - expect(transaction.request?.data).toBe(`${'a'.repeat(997)}...`); + span: container => { + const span = container.items.find(span => span.is_segment); + expect(span).toBeDefined(); + expect(span?.attributes['http.request.body.data']?.value).toBe(`${'a'.repeat(997)}...`); }, }) .start(); @@ -54,9 +65,11 @@ test('an explicit none overrides enabled body collection', async () => { .withMockSentryServer() .withEnv({ BODY_MODE: 'explicit-none' }) .expect({ - transaction: transaction => { - expect(transaction.request?.method).toBe('POST'); - expect(transaction.request?.data).toBeUndefined(); + span: container => { + const span = container.items.find(span => span.is_segment); + expect(span).toBeDefined(); + expect(span?.attributes['http.request.method']?.value).toBe('POST'); + expect(span?.attributes['http.request.body.data']).toBeUndefined(); }, }) .start(); From 880a6ac5fc3e6f5ff89de34fc043ba5c89c4e2e5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 10:14:02 +0200 Subject: [PATCH 04/73] test(node): port rejection, feature flag, and client-report tests to span streaming (#25020) Move rejection, feature-flag, GrowthBook, and client-report coverage to the default span-streaming lifecycle. Preserve existing assertions while adapting transaction checks to streamed spans and allowing span/error envelopes to arrive in either order. Part of #24141 (JS-3611) --------- Co-authored-by: GPT-6 --- .../drop-reasons/before-send/scenario.ts | 1 - .../event-processor-throws/scenario-async.ts | 1 - .../drop-reasons/event-processors/scenario.ts | 1 - .../scenario-fallback.ts | 1 - .../traces-sampler-throws/test.ts | 8 ++-- .../onSpan/scenario.ts | 1 - .../featureFlagsIntegration/onSpan/test.ts | 35 ++++++++++------- .../growthbook/onSpan/scenario.ts | 1 - .../featureFlags/growthbook/onSpan/test.ts | 36 +++++++++--------- .../ignore-custom-name.js | 1 - .../ignore-default.js | 1 - .../mode-none.js | 1 - .../mode-strict.js | 1 - .../mode-warn-error.js | 1 - .../mode-warn-string.js | 1 - .../scenario-strict.ts | 1 - .../scenario-warn.ts | 1 - .../scenario-with-span-ended.ts | 1 - .../scenario-with-span.ts | 1 - .../onUnhandledRejectionIntegration/test.ts | 38 +++++++++---------- 20 files changed, 63 insertions(+), 70 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts index 2b90554e068b..4c6f464b62f9 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts @@ -4,7 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, beforeSend(event) { diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts index bade2d046447..f92604e5971d 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processor-throws/scenario-async.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts index 56a4eef4831b..70af7e321108 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts @@ -4,7 +4,6 @@ import { loggingTransport } from '@sentry-internal/node-integration-tests'; // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts index 9d5524afe3ab..8f838322f5ec 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/scenario-fallback.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, tracesSampleRate: 1, diff --git a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts index e511fc97e8a9..965985ea78d0 100644 --- a/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts +++ b/dev-packages/node-integration-tests/suites/client-reports/drop-reasons/traces-sampler-throws/test.ts @@ -1,4 +1,4 @@ -import { afterAll, test } from 'vitest'; +import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { @@ -25,9 +25,11 @@ test('records a client report and no error event when tracesSampler throws', asy test('sends the span when tracesSampler throws but tracesSampleRate is 1', async () => { await createRunner(__dirname, 'scenario-fallback.ts') + .unordered() .expect({ - transaction: { - transaction: 'sampled via tracesSampleRate fallback', + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('sampled via tracesSampleRate fallback'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts index 2f78fabdebe9..2c07e46b40ed 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts index 4a417a3c3959..b62c330ce81e 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN, type SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; @@ -8,24 +8,31 @@ afterAll(() => { test('Flags captured on span attributes with max limit', async () => { // Based on scenario.ts. - const expectedFlags: Record = {}; + const expectedFlags: Record = {}; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedFlags[`flag.evaluation.feat${i}`] = i === 3; + expectedFlags[`flag.evaluation.feat${i}`] = { type: 'boolean', value: i === 3 }; } + const spans: SerializedStreamedSpanContainer['items'] = []; await createRunner(__dirname, 'scenario.ts') + .unordered() .expect({ - transaction: { - spans: [ - expect.objectContaining({ - description: 'test-span', - data: expect.objectContaining({}), - }), - expect.objectContaining({ - description: 'test-nested-span', - data: expect.objectContaining(expectedFlags), - }), - ], + span: container => { + spans.push(...container.items); + const children = spans.filter(span => !span.is_segment); + expect(children).toHaveLength(2); + expect(children).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'test-span', + attributes: expect.objectContaining({}), + }), + expect.objectContaining({ + name: 'test-nested-span', + attributes: expect.objectContaining(expectedFlags), + }), + ]), + ); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts index 7f9da04fdbd8..b25f36f00951 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts @@ -44,7 +44,6 @@ class GrowthBookWrapper { const gb = new GrowthBookWrapper(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts index 3f0011b4f617..8f5d4fdbdf9b 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts @@ -7,26 +7,26 @@ afterAll(() => { test('GrowthBook flags are added to active span attributes on span end', async () => { await createRunner(__dirname, 'scenario.ts') + .unordered() .expect({ - transaction: { - contexts: { - trace: { - data: { - 'flag.evaluation.feat1': true, - 'flag.evaluation.feat2': false, - 'flag.evaluation.bool-feat': true, - // string-feat should NOT be here since it's not boolean - }, - op: 'function', - origin: 'manual', - status: 'ok', - span_id: expect.stringMatching(/[a-f\d]{16}/), - trace_id: expect.stringMatching(/[a-f\d]{32}/), + span: container => { + expect(container.items).toHaveLength(1); + const span = container.items.find(item => item.is_segment); + expect(span).toMatchObject({ + name: 'test-span', + status: 'ok', + span_id: expect.stringMatching(/[a-f\d]{16}/), + trace_id: expect.stringMatching(/[a-f\d]{32}/), + attributes: { + 'flag.evaluation.feat1': { type: 'boolean', value: true }, + 'flag.evaluation.feat2': { type: 'boolean', value: false }, + 'flag.evaluation.bool-feat': { type: 'boolean', value: true }, + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'manual' }, }, - }, - spans: [], - transaction: 'test-span', - type: 'transaction', + }); + // string-feat should NOT be here since it's not boolean + expect(span?.attributes['flag.evaluation.string-feat']).toBeUndefined(); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js index 5005a1bc3e2c..f85ebfada002 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.onUnhandledRejectionIntegration({ diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js index 84676901b052..d59cd557a4cb 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', // Use default mode: 'warn' - integration is active but should ignore AI_NoOutputGeneratedError }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js index 5db0588888ec..cad5ce1c8fe5 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'none' })], }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js index 1eeaf1e71cab..841668b4b4ac 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })], }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js index a9d3648132b8..45a84bbf5594 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js index 2be232cfa92e..79ea78de095f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js @@ -2,7 +2,6 @@ const Sentry = require('@sentry/node'); const { expectProcessToExit } = require('../../../utils/expect-process-to-exit'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts index 053a73a4b790..a26e19de58d3 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts index 9ed5ee4a447a..24b3093a554c 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts index af022a693202..72d83d70ec72 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts index 214ab0a52855..edff30f114ca 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1, diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts index 0cc15ce97173..199f3dffe5aa 100644 --- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts @@ -1,4 +1,4 @@ -import type { Event } from '@sentry/node'; +import type { Event, SerializedStreamedSpanContainer } from '@sentry/node'; import * as childProcess from 'child_process'; import * as path from 'path'; import { afterAll, describe, expect, test } from 'vitest'; @@ -142,15 +142,16 @@ test rejection`); .completed(); }); - // Bun: the error event arrives before the transaction, or has a different span id. - test.skipIf(RUNTIME === 'bun')('handles unhandled rejection in spans', async () => { - let transactionEvent: Event | undefined; + test('handles unhandled rejection in spans', async () => { + let segment: SerializedStreamedSpanContainer['items'][number] | undefined; let errorEvent: Event | undefined; await createRunner(__dirname, 'scenario-with-span.ts') + .unordered() .expect({ - transaction: transaction => { - transactionEvent = transaction; + span: container => { + segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('test-span'); }, }) .expect({ @@ -161,23 +162,24 @@ test rejection`); .start() .completed(); - expect(transactionEvent).toBeDefined(); + expect(segment).toBeDefined(); expect(errorEvent).toBeDefined(); - expect(transactionEvent!.transaction).toBe('test-span'); - - expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); - expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); + expect(segment!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); + expect(segment!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); }); + // Bun: the rejection event's span ID differs from the already-ended span's ID. test.skipIf(RUNTIME === 'bun')('handles unhandled rejection in spans that are ended early', async () => { - let transactionEvent: Event | undefined; + let segment: SerializedStreamedSpanContainer['items'][number] | undefined; let errorEvent: Event | undefined; await createRunner(__dirname, 'scenario-with-span-ended.ts') + .unordered() .expect({ - transaction: transaction => { - transactionEvent = transaction; + span: container => { + segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('test-span'); }, }) .expect({ @@ -188,13 +190,11 @@ test rejection`); .start() .completed(); - expect(transactionEvent).toBeDefined(); + expect(segment).toBeDefined(); expect(errorEvent).toBeDefined(); - expect(transactionEvent!.transaction).toBe('test-span'); - - expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); - expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); + expect(segment!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); + expect(segment!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); }); test('should not warn when AI_NoOutputGeneratedError or AbortError is rejected (default ignore)', () => From 566d7d8e6df5bfb0ef5a8987417ae096913d4525 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 10:14:18 +0200 Subject: [PATCH 05/73] test(node): port AWS integration tests to span streaming (#25018) Move AWS integration coverage to the default span-streaming lifecycle and fold the streamed twin into the original suite. Preserve the existing assertions and both SDK-version variants in ESM and CJS, collecting spans across envelopes before asserting. Part of #24141 (JS-3611). Co-authored-by: GPT-6 --- .../aws-integration-streamed/instrument.mjs | 10 - .../aws-integration-streamed/scenario.mjs | 183 ----------- .../aws-integration-streamed/test.ts | 249 --------------- .../aws-integration/instrument.mjs | 1 - .../aws-serverless/aws-integration/test.ts | 286 ++++++++++-------- 5 files changed, 162 insertions(+), 567 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs delete mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs delete mode 100644 dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs deleted file mode 100644 index abfa58bbff9f..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/instrument.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import * as Sentry from '@sentry/aws-serverless'; -import { loggingTransport } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, - traceLifecycle: 'stream', -}); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs deleted file mode 100644 index 624a10e9523b..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/scenario.mjs +++ /dev/null @@ -1,183 +0,0 @@ -import * as Sentry from '@sentry/aws-serverless'; -import { createHash } from 'crypto'; -import { DynamoDBClient, PutItemCommand, QueryCommand } from '@aws-sdk/client-dynamodb'; -import { KinesisClient, PutRecordCommand } from '@aws-sdk/client-kinesis'; -import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; -import { S3 } from '@aws-sdk/client-s3'; -import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; -import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn'; -import { PublishCommand, SNSClient } from '@aws-sdk/client-sns'; -import { ReceiveMessageCommand, SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; -// The Kinesis client defaults to an HTTP/2 request handler, which `nock` cannot intercept. -// Force the HTTP/1 handler so the request is mocked instead of hitting real AWS. -import { NodeHttpHandler } from '@smithy/node-http-handler'; -import nock from 'nock'; - -nock.disableNetConnect(); - -const region = 'us-east-1'; -const credentials = { accessKeyId: 'aws-test-key', secretAccessKey: 'aws-test-secret' }; - -async function s3() { - const s3Client = new S3({ region, credentials, maxAttempts: 1 }); - const host = `https://ot-demo-test.s3.${region}.amazonaws.com`; - - nock(host).put('/aws-ot-s3-test-object.txt?x-id=PutObject').reply(200, 'test'); - await s3Client.putObject({ Bucket: 'ot-demo-test', Key: 'aws-ot-s3-test-object.txt' }); - - nock(host).get('/aws-ot-s3-test-object.txt?x-id=GetObject').reply(200, 'contents'); - const getResult = await s3Client.getObject({ Bucket: 'ot-demo-test', Key: 'aws-ot-s3-test-object.txt' }); - await getResult.Body?.transformToString(); - - nock(host) - .get('/missing-object.txt?x-id=GetObject') - .reply( - 404, - 'NoSuchKeyThe specified key does not exist.', - { 'content-type': 'application/xml' }, - ); - try { - await s3Client.getObject({ Bucket: 'ot-demo-test', Key: 'missing-object.txt' }); - } catch { - // expected - } -} - -async function dynamodb() { - const client = new DynamoDBClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://dynamodb.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({}), { 'content-type': 'application/x-amz-json-1.0' }); - await client.send(new PutItemCommand({ TableName: 'my-table', Item: { id: { S: 'some-id' } } })); - - nock(`https://dynamodb.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ Items: [{ id: { S: 'some-id' } }], Count: 1, ScannedCount: 1 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send( - new QueryCommand({ - TableName: 'my-table', - KeyConditionExpression: 'id = :id', - ExpressionAttributeValues: { ':id': { S: 'some-id' } }, - }), - ); -} - -async function sqs() { - const client = new SQSClient({ region, credentials, maxAttempts: 1 }); - const queueUrl = `https://sqs.${region}.amazonaws.com/123456789012/my-queue`; - const messageBody = 'Hello from Sentry'; - const md5 = createHash('md5').update(messageBody).digest('hex'); - - nock(`https://sqs.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ MessageId: 'message-id-1', MD5OfMessageBody: md5 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: messageBody })); - - nock(`https://sqs.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - JSON.stringify({ - Messages: [{ MessageId: 'message-id-2', Body: messageBody, MD5OfBody: md5, ReceiptHandle: 'handle' }], - }), - { 'content-type': 'application/x-amz-json-1.0' }, - ); - await client.send(new ReceiveMessageCommand({ QueueUrl: queueUrl })); -} - -async function sns() { - const client = new SNSClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://sns.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - 'message-id-1request-id-1', - { 'content-type': 'text/xml' }, - ); - await client.send(new PublishCommand({ TopicArn: 'arn:aws:sns:us-east-1:123456789012:my-topic', Message: 'Hello' })); - - // Publish to a platform endpoint, whose ARN ends in a per-device id. - nock(`https://sns.${region}.amazonaws.com`) - .post('/') - .reply( - 200, - 'message-id-2request-id-2', - { 'content-type': 'text/xml' }, - ); - await client.send( - new PublishCommand({ - TargetArn: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - Message: 'Hello', - }), - ); -} - -async function lambda() { - const client = new LambdaClient({ region, credentials, maxAttempts: 1 }); - - nock(`https://lambda.${region}.amazonaws.com`) - .post('/2015-03-31/functions/my-function/invocations') - .reply(200, JSON.stringify({ result: 'ok' }), { - 'content-type': 'application/json', - 'x-amzn-requestid': 'request-id-1', - }); - await client.send(new InvokeCommand({ FunctionName: 'my-function' })); -} - -async function kinesis() { - const client = new KinesisClient({ region, credentials, maxAttempts: 1, requestHandler: new NodeHttpHandler() }); - - nock(`https://kinesis.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ SequenceNumber: '1', ShardId: 'shardId-000000000000' }), { - 'content-type': 'application/x-amz-json-1.1', - }); - await client.send( - new PutRecordCommand({ StreamName: 'my-stream', Data: Buffer.from('data'), PartitionKey: 'partition-key' }), - ); -} - -async function secretsmanager() { - const client = new SecretsManagerClient({ region, credentials, maxAttempts: 1 }); - const secretArn = 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc'; - - nock(`https://secretsmanager.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ ARN: secretArn, Name: 'my-secret', SecretString: 'secret-value' }), { - 'content-type': 'application/x-amz-json-1.1', - }); - await client.send(new GetSecretValueCommand({ SecretId: secretArn })); -} - -async function stepfunctions() { - const client = new SFNClient({ region, credentials, maxAttempts: 1 }); - const stateMachineArn = 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine'; - - nock(`https://states.${region}.amazonaws.com`) - .post('/') - .reply(200, JSON.stringify({ executionArn: `${stateMachineArn}:execution-1`, startDate: 1 }), { - 'content-type': 'application/x-amz-json-1.0', - }); - await client.send(new StartExecutionCommand({ stateMachineArn, input: '{}' })); -} - -async function run() { - await Sentry.startSpan({ name: 'Test Service Span' }, async () => { - await s3(); - await dynamodb(); - await sqs(); - await sns(); - await lambda(); - await kinesis(); - await secretsmanager(); - await stepfunctions(); - }); -} - -run(); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts deleted file mode 100644 index b4974f86ef2e..000000000000 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration-streamed/test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { afterAll, describe, expect } from 'vitest'; -import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; - -// See the non-streamed `aws-integration` suite: only the origin differs between the OTel and -// orchestrion diagnostics-channel runs. -const ORIGIN = 'auto.aws.aws_sdk'; - -// The aws-sdk instrumentation creates spans by patching the underlying smithy middleware stack. The -// patch target differs between aws-sdk versions, so we run the exact same assertions against both: -// - the current aws-sdk (default, resolved from the workspace) which routes through `@smithy/core` >= 3.24.0 -// - aws-sdk 3.1041.0 pinned together with the pre-`@smithy/core` stack (`@smithy/middleware-stack`) -const LEGACY_AWS_SDK_DEPENDENCIES = { - '@aws-sdk/client-dynamodb': '3.1041.0', - '@aws-sdk/client-kinesis': '3.1041.0', - '@aws-sdk/client-lambda': '3.1041.0', - '@aws-sdk/client-s3': '3.1041.0', - '@aws-sdk/client-secrets-manager': '3.1041.0', - '@aws-sdk/client-sfn': '3.1041.0', - '@aws-sdk/client-sns': '3.1041.0', - '@aws-sdk/client-sqs': '3.1041.0', - // Pin the smithy layer to the pre-`@smithy/core` versions, otherwise the 3.1041.0 clients still - // resolve `@smithy/smithy-client` >= 4.13 (which routes through `@smithy/core` >= 3.24.0). - '@smithy/smithy-client': '4.12.13', - '@smithy/core': '3.23.17', - '@smithy/middleware-stack': '4.2.14', - '@smithy/node-http-handler': '4.7.8', -}; - -/** - * Asserts the transaction contains one span per instrumented aws-sdk service. Each service is checked - * with its own `expect` so a failure points at the specific service rather than the whole transaction. - */ -function assertAwsServiceSpans(spanCcontainer: SerializedStreamedSpanContainer): void { - const spans = spanCcontainer.items; - - const expectSpan = ( - label: string, - expected: Record, - find?: (item: SerializedStreamedSpanContainer['items'][number]) => boolean, - ): void => { - const matches = spans.filter(item => item.name === expected.name); - const span = find ? matches.find(find) : matches[0]; - expect(span, label).toMatchObject(expected); - }; - - const segmentSpan = spans.find(item => item.is_segment); - - expect(segmentSpan?.name).toBe('Test Service Span'); - - // S3 - PutObject (success) - expectSpan('S3.PutObject', { - name: 'S3.PutObject', - status: 'ok', - attributes: expect.objectContaining({ - 'sentry.origin': { value: ORIGIN, type: 'string' }, - 'sentry.op': { value: 'rpc', type: 'string' }, - 'rpc.system': { value: 'aws-api', type: 'string' }, - 'rpc.method': { value: 'PutObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - 'cloud.region': { value: 'us-east-1', type: 'string' }, - 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, - 'sentry.kind': { value: 'client', type: 'string' }, - }), - }); - - // S3 - GetObject (success) - expectSpan( - 'S3.GetObject (success)', - { - name: 'S3.GetObject', - status: 'ok', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, - }), - }, - // Two spans share the name `S3.GetObject`; disambiguate by HTTP status code. - item => item.attributes['http.response.status_code']?.value === 200, - ); - - // S3 - GetObject (errored, missing key) - expectSpan( - 'S3.GetObject (error)', - { - name: 'S3.GetObject', - status: 'error', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetObject', type: 'string' }, - 'rpc.service': { value: 'S3', type: 'string' }, - }), - }, - item => item.attributes['http.response.status_code']?.value === 404, - ); - - // DynamoDB - PutItem - expectSpan('DynamoDB.PutItem', { - name: 'DynamoDB.PutItem', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'PutItem', type: 'string' }, - 'rpc.service': { value: 'DynamoDB', type: 'string' }, - 'db.system.name': { value: 'dynamodb', type: 'string' }, - 'db.namespace': { value: 'my-table', type: 'string' }, - 'db.operation.name': { value: 'PutItem', type: 'string' }, - 'aws.dynamodb.table_names': { value: ['my-table'], type: 'array' }, - }), - }); - - // DynamoDB - Query - expectSpan('DynamoDB.Query', { - name: 'DynamoDB.Query', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Query', type: 'string' }, - 'db.operation.name': { value: 'Query', type: 'string' }, - 'aws.dynamodb.count': { value: 1, type: 'integer' }, - 'aws.dynamodb.scanned_count': { value: 1, type: 'integer' }, - }), - }); - - // SQS - SendMessage (producer) - expectSpan('SQS SendMessage', { - name: 'send my-queue', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'rpc.method': { value: 'SendMessage', type: 'string' }, - 'rpc.service': { value: 'SQS', type: 'string' }, - 'messaging.system': { value: 'aws_sqs', type: 'string' }, - 'messaging.destination.name': { value: 'my-queue', type: 'string' }, - 'url.full': { value: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', type: 'string' }, - 'messaging.message.id': { value: 'message-id-1', type: 'string' }, - 'sentry.kind': { value: 'producer', type: 'string' }, - }), - }); - - // SQS - ReceiveMessage (consumer) - expectSpan('SQS ReceiveMessage', { - name: 'receive my-queue', - attributes: expect.objectContaining({ - 'sentry.op': { value: 'queue.receive', type: 'string' }, - 'rpc.method': { value: 'ReceiveMessage', type: 'string' }, - 'messaging.system': { value: 'aws_sqs', type: 'string' }, - 'messaging.operation.type': { value: 'receive', type: 'string' }, - 'messaging.batch.message_count': { value: 1, type: 'integer' }, - 'sentry.kind': { value: 'consumer', type: 'string' }, - }), - }); - - // SNS - Publish (producer) - expectSpan('SNS Publish', { - name: 'send my-topic', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Publish', type: 'string' }, - 'rpc.service': { value: 'SNS', type: 'string' }, - 'messaging.system': { value: 'aws.sns', type: 'string' }, - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'messaging.destination': { value: 'my-topic', type: 'string' }, - 'aws.sns.topic.arn': { value: 'arn:aws:sns:us-east-1:123456789012:my-topic', type: 'string' }, - 'sentry.kind': { value: 'producer', type: 'string' }, - }), - }); - - // The ARN suffix is a per-device id, so the streamed name drops the destination. - expectSpan('SNS Publish (platform endpoint)', { - name: 'send', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Publish', type: 'string' }, - 'rpc.service': { value: 'SNS', type: 'string' }, - 'sentry.op': { value: 'queue.publish', type: 'string' }, - 'messaging.destination': { value: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', type: 'string' }, - 'messaging.destination.name': { - value: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - type: 'string', - }, - }), - }); - - // Lambda - Invoke - expectSpan('Lambda Invoke', { - name: 'my-function Invoke', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'Invoke', type: 'string' }, - 'rpc.service': { value: 'Lambda', type: 'string' }, - 'faas.invoked_name': { value: 'my-function', type: 'string' }, - 'faas.invoked_provider': { value: 'aws', type: 'string' }, - 'faas.invocation_id': { value: 'request-id-1', type: 'string' }, - }), - }); - - // Kinesis - PutRecord - expectSpan('Kinesis.PutRecord', { - name: 'Kinesis.PutRecord', - status: 'ok', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'PutRecord', type: 'string' }, - 'rpc.service': { value: 'Kinesis', type: 'string' }, - 'aws.kinesis.stream.name': { value: 'my-stream', type: 'string' }, - }), - }); - - // SecretsManager - GetSecretValue - expectSpan('SecretsManager.GetSecretValue', { - name: 'SecretsManager.GetSecretValue', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'GetSecretValue', type: 'string' }, - 'rpc.service': { value: 'SecretsManager', type: 'string' }, - 'aws.secretsmanager.secret.arn': { - value: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', - type: 'string', - }, - }), - }); - - // StepFunctions - StartExecution - expectSpan('StepFunctions.StartExecution', { - name: 'SFN.StartExecution', - attributes: expect.objectContaining({ - 'rpc.method': { value: 'StartExecution', type: 'string' }, - 'rpc.service': { value: 'SFN', type: 'string' }, - 'aws.step_functions.state_machine.arn': { - value: 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', - type: 'string', - }, - }), - }); -} - -describe('awsIntegration (streamed)', () => { - afterAll(() => { - cleanupChildProcesses(); - }); - - describe.each([ - { label: 'latest', additionalDependencies: undefined }, - { label: 'v3.1041.0 (@smithy/middleware-stack)', additionalDependencies: LEGACY_AWS_SDK_DEPENDENCIES }, - ])('aws-sdk $label', ({ additionalDependencies }) => { - createEsmAndCjsTests( - __dirname, - 'scenario.mjs', - 'instrument.mjs', - (createTestRunner, test) => { - test('auto-instruments aws-sdk service operations', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ span: assertAwsServiceSpans }).start().completed(); - }); - }, - { additionalDependencies }, - ); - }); -}); diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs index d6a9efde7bae..fe1c5c47983f 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/aws-serverless'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts index cac71f20ad6e..e12bf1ecaff2 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/aws-integration/test.ts @@ -1,4 +1,4 @@ -import type { TransactionEvent } from '@sentry/core'; +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -29,189 +29,216 @@ const LEGACY_AWS_SDK_DEPENDENCIES = { }; /** - * Asserts the transaction contains one span per instrumented aws-sdk service. Each service is checked - * with its own `expect` so a failure points at the specific service rather than the whole transaction. + * Asserts the collected spans include one span per instrumented aws-sdk service. Each service is checked + * with its own `expect` so a failure points at the specific service rather than the whole trace. */ -function assertAwsServiceSpans(transaction: TransactionEvent): void { - const spans = transaction.spans ?? []; - - const expectSpan = (label: string, expected: Record): void => { - expect(spans, `expected an aws-sdk span for "${label}"`).toContainEqual(expect.objectContaining(expected)); +function assertAwsServiceSpans(spans: SerializedStreamedSpanContainer['items']): void { + const expectSpan = ( + label: string, + expected: Record, + find?: (item: SerializedStreamedSpanContainer['items'][number]) => boolean, + ): void => { + const matches = spans.filter(item => item.name === expected.name); + const span = find ? matches.find(find) : matches[0]; + expect(span, label).toMatchObject(expected); }; - expect(transaction.transaction).toBe('Test Transaction'); + const segmentSpan = spans.find(item => item.is_segment); + + expect(segmentSpan?.name).toBe('Test Transaction'); // S3 - PutObject (success) expectSpan('S3.PutObject', { - description: 'S3.PutObject', - op: 'rpc', - origin: ORIGIN, + name: 'S3.PutObject', status: 'ok', - data: expect.objectContaining({ - 'sentry.origin': ORIGIN, - 'sentry.op': 'rpc', - 'rpc.system': 'aws-api', - 'rpc.method': 'PutObject', - 'rpc.service': 'S3', - 'cloud.region': 'us-east-1', - 'aws.s3.bucket': 'ot-demo-test', - 'sentry.kind': 'client', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'rpc', type: 'string' }, + 'rpc.system': { value: 'aws-api', type: 'string' }, + 'rpc.method': { value: 'PutObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + 'cloud.region': { value: 'us-east-1', type: 'string' }, + 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, + 'sentry.kind': { value: 'client', type: 'string' }, }), }); // S3 - GetObject (success) - expectSpan('S3.GetObject (success)', { - description: 'S3.GetObject', - op: 'rpc', - origin: ORIGIN, - status: 'ok', - data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3', 'aws.s3.bucket': 'ot-demo-test' }), - }); + expectSpan( + 'S3.GetObject (success)', + { + name: 'S3.GetObject', + status: 'ok', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + 'aws.s3.bucket': { value: 'ot-demo-test', type: 'string' }, + }), + }, + // Two spans share the name `S3.GetObject`; disambiguate by HTTP status code. + item => item.attributes['http.response.status_code']?.value === 200, + ); // S3 - GetObject (errored, missing key) - expectSpan('S3.GetObject (error)', { - description: 'S3.GetObject', - op: 'rpc', - origin: ORIGIN, - status: 'internal_error', - data: expect.objectContaining({ 'rpc.method': 'GetObject', 'rpc.service': 'S3' }), - }); + expectSpan( + 'S3.GetObject (error)', + { + name: 'S3.GetObject', + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetObject', type: 'string' }, + 'rpc.service': { value: 'S3', type: 'string' }, + }), + }, + item => item.attributes['http.response.status_code']?.value === 404, + ); // DynamoDB - PutItem expectSpan('DynamoDB.PutItem', { - description: 'DynamoDB.PutItem', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'sentry.op': 'db', - 'rpc.method': 'PutItem', - 'rpc.service': 'DynamoDB', - 'db.system.name': 'dynamodb', - 'db.namespace': 'my-table', - 'db.operation.name': 'PutItem', - 'aws.dynamodb.table_names': ['my-table'], + name: 'DynamoDB.PutItem', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'PutItem', type: 'string' }, + 'rpc.service': { value: 'DynamoDB', type: 'string' }, + 'db.system.name': { value: 'dynamodb', type: 'string' }, + 'db.namespace': { value: 'my-table', type: 'string' }, + 'db.operation.name': { value: 'PutItem', type: 'string' }, + 'aws.dynamodb.table_names': { value: ['my-table'], type: 'array' }, }), }); // DynamoDB - Query expectSpan('DynamoDB.Query', { - description: 'DynamoDB.Query', - op: 'db', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Query', - 'db.operation.name': 'Query', - 'aws.dynamodb.count': 1, - 'aws.dynamodb.scanned_count': 1, + name: 'DynamoDB.Query', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'db', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Query', type: 'string' }, + 'db.operation.name': { value: 'Query', type: 'string' }, + 'aws.dynamodb.count': { value: 1, type: 'integer' }, + 'aws.dynamodb.scanned_count': { value: 1, type: 'integer' }, }), }); // SQS - SendMessage (producer) expectSpan('SQS SendMessage', { - description: 'my-queue send', - op: 'queue.publish', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'SendMessage', - 'rpc.service': 'SQS', - 'messaging.system': 'aws_sqs', - 'messaging.destination.name': 'my-queue', - 'url.full': 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', - 'messaging.message.id': 'message-id-1', - 'sentry.kind': 'producer', + name: 'send my-queue', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'rpc.method': { value: 'SendMessage', type: 'string' }, + 'rpc.service': { value: 'SQS', type: 'string' }, + 'messaging.system': { value: 'aws_sqs', type: 'string' }, + 'messaging.destination.name': { value: 'my-queue', type: 'string' }, + 'url.full': { value: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue', type: 'string' }, + 'messaging.message.id': { value: 'message-id-1', type: 'string' }, + 'sentry.kind': { value: 'producer', type: 'string' }, }), }); // SQS - ReceiveMessage (consumer) expectSpan('SQS ReceiveMessage', { - description: 'my-queue receive', - op: 'queue.receive', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'ReceiveMessage', - 'messaging.system': 'aws_sqs', - 'messaging.operation.type': 'receive', - 'messaging.batch.message_count': 1, - 'sentry.kind': 'consumer', + name: 'receive my-queue', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'sentry.op': { value: 'queue.receive', type: 'string' }, + 'rpc.method': { value: 'ReceiveMessage', type: 'string' }, + 'messaging.system': { value: 'aws_sqs', type: 'string' }, + 'messaging.operation.type': { value: 'receive', type: 'string' }, + 'messaging.batch.message_count': { value: 1, type: 'integer' }, + 'sentry.kind': { value: 'consumer', type: 'string' }, }), }); // SNS - Publish (producer) expectSpan('SNS Publish', { - description: 'my-topic send', - op: 'queue.publish', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Publish', - 'rpc.service': 'SNS', - 'messaging.system': 'aws.sns', - 'messaging.destination': 'my-topic', - 'aws.sns.topic.arn': 'arn:aws:sns:us-east-1:123456789012:my-topic', - 'sentry.kind': 'producer', + name: 'send my-topic', + attributes: expect.objectContaining({ + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Publish', type: 'string' }, + 'rpc.service': { value: 'SNS', type: 'string' }, + 'messaging.system': { value: 'aws.sns', type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'messaging.destination': { value: 'my-topic', type: 'string' }, + 'aws.sns.topic.arn': { value: 'arn:aws:sns:us-east-1:123456789012:my-topic', type: 'string' }, + 'sentry.kind': { value: 'producer', type: 'string' }, }), }); - // Without span streaming the name keeps the raw ARN suffix, including the per-device id. + // The ARN suffix is a per-device id, so the streamed name drops the destination. expectSpan('SNS Publish (platform endpoint)', { - description: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3 send', - op: 'queue.publish', - data: expect.objectContaining({ - 'rpc.method': 'Publish', - 'rpc.service': 'SNS', - 'messaging.destination': 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', - 'messaging.destination.name': - 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', + name: 'send', + attributes: expect.objectContaining({ + 'rpc.method': { value: 'Publish', type: 'string' }, + 'rpc.service': { value: 'SNS', type: 'string' }, + 'sentry.op': { value: 'queue.publish', type: 'string' }, + 'messaging.destination': { value: 'endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', type: 'string' }, + 'messaging.destination.name': { + value: 'arn:aws:sns:us-east-1:123456789012:endpoint/GCM/myapp/5e3e9847-3183-3f18-a7e8-671c3a57d4b3', + type: 'string', + }, }), }); // Lambda - Invoke expectSpan('Lambda Invoke', { - description: 'my-function Invoke', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'Invoke', - 'rpc.service': 'Lambda', - 'faas.invoked_name': 'my-function', - 'faas.invoked_provider': 'aws', - 'faas.invocation_id': 'request-id-1', + name: 'my-function Invoke', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'Invoke', type: 'string' }, + 'rpc.service': { value: 'Lambda', type: 'string' }, + 'faas.invoked_name': { value: 'my-function', type: 'string' }, + 'faas.invoked_provider': { value: 'aws', type: 'string' }, + 'faas.invocation_id': { value: 'request-id-1', type: 'string' }, }), }); // Kinesis - PutRecord expectSpan('Kinesis.PutRecord', { - description: 'Kinesis.PutRecord', - op: 'rpc', - origin: ORIGIN, + name: 'Kinesis.PutRecord', status: 'ok', - data: expect.objectContaining({ - 'rpc.method': 'PutRecord', - 'rpc.service': 'Kinesis', - 'aws.kinesis.stream.name': 'my-stream', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'PutRecord', type: 'string' }, + 'rpc.service': { value: 'Kinesis', type: 'string' }, + 'aws.kinesis.stream.name': { value: 'my-stream', type: 'string' }, }), }); // SecretsManager - GetSecretValue expectSpan('SecretsManager.GetSecretValue', { - description: 'SecretsManager.GetSecretValue', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'GetSecretValue', - 'rpc.service': 'SecretsManager', - 'aws.secretsmanager.secret.arn': 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', + name: 'SecretsManager.GetSecretValue', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'GetSecretValue', type: 'string' }, + 'rpc.service': { value: 'SecretsManager', type: 'string' }, + 'aws.secretsmanager.secret.arn': { + value: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret-abc', + type: 'string', + }, }), }); // StepFunctions - StartExecution expectSpan('StepFunctions.StartExecution', { - description: 'SFN.StartExecution', - op: 'rpc', - origin: ORIGIN, - data: expect.objectContaining({ - 'rpc.method': 'StartExecution', - 'rpc.service': 'SFN', - 'aws.step_functions.state_machine.arn': 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', + name: 'SFN.StartExecution', + attributes: expect.objectContaining({ + 'sentry.op': { value: 'rpc', type: 'string' }, + 'sentry.origin': { value: ORIGIN, type: 'string' }, + 'rpc.method': { value: 'StartExecution', type: 'string' }, + 'rpc.service': { value: 'SFN', type: 'string' }, + 'aws.step_functions.state_machine.arn': { + value: 'arn:aws:states:us-east-1:123456789012:stateMachine:my-state-machine', + type: 'string', + }, }), }); } @@ -231,7 +258,18 @@ describe('awsIntegration', () => { 'instrument.mjs', (createTestRunner, test) => { test('auto-instruments aws-sdk service operations', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ transaction: assertAwsServiceSpans }).start().completed(); + const spans: SerializedStreamedSpanContainer['items'] = []; + await createTestRunner() + .ignore('event') + .unordered() + .expect({ + span: container => { + spans.push(...container.items); + assertAwsServiceSpans(spans); + }, + }) + .start() + .completed(); }); }, { additionalDependencies }, From 5f2f5f57a066e654ebcf913a8a59640e449945d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Mon, 5 Oct 2026 11:43:35 +0300 Subject: [PATCH 06/73] test(e2e): Move react-router-8-cloudflare MySQL tests into react-router-8-framework (#24599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit part of #24836 With the Cloudflare variant of `react-router-8-framework`, the separate `react-router-8-cloudflare` app only added its MySQL test. This moves the MySQL route and both tests into `react-router-8-framework` and removes the old app, so one app covers React Router on Node, Bun, Deno and Cloudflare. The MySQL tests now run on every variant, not only on Cloudflare. The Worker is wrapped by `sentryCloudflareVitePlugin` instead of a manual `withSentry` call; other Cloudflare apps still cover manual `withSentry`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 --- .../react-router-8-cloudflare/.gitignore | 35 ------------ .../app/entry.client.tsx | 22 -------- .../app/entry.server.tsx | 51 ----------------- .../react-router-8-cloudflare/app/root.tsx | 23 -------- .../react-router-8-cloudflare/app/routes.ts | 6 -- .../app/routes/home.tsx | 10 ---- .../docker-compose.yml | 18 ------ .../global-setup.mjs | 13 ----- .../global-teardown.mjs | 12 ---- .../react-router-8-cloudflare/package.json | 45 --------------- .../playwright.config.mjs | 14 ----- .../public/favicon.ico | Bin 15086 -> 0 bytes .../react-router.config.ts | 5 -- .../start-event-proxy.mjs | 6 -- .../tests/db.test.ts | 50 ----------------- .../react-router-8-cloudflare/tsconfig.json | 21 ------- .../react-router-8-cloudflare/vite.config.ts | 13 ----- .../react-router-8-cloudflare/workers/app.ts | 26 --------- .../react-router-8-cloudflare/wrangler.jsonc | 13 ----- .../react-router-8-framework/app/routes.ts | 1 + .../app/routes/performance/db-mysql.tsx | 4 +- .../docker-compose.yml | 17 ++++++ .../react-router-8-framework/global-setup.mjs | 2 +- .../react-router-8-framework/package.json | 4 +- .../tests/performance/mysql.server.test.ts | 53 ++++++++++++++++++ 25 files changed, 77 insertions(+), 387 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc rename dev-packages/e2e-tests/test-applications/{react-router-8-cloudflare => react-router-8-framework}/app/routes/performance/db-mysql.tsx (87%) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore deleted file mode 100644 index e637ae342782..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -/test-results/ -/playwright-report/ -/playwright/.cache/ - -!*.d.ts - -# react router -.react-router - -# cloudflare -.wrangler diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx deleted file mode 100644 index 0d928d68760a..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as Sentry from '@sentry/react-router'; -import { startTransition, StrictMode } from 'react'; -import { hydrateRoot } from 'react-dom/client'; -import { HydratedRouter } from 'react-router/dom'; - -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: 'https://username@domain/123', - tunnel: `http://localhost:3031/`, // proxy server - integrations: [Sentry.reactRouterTracingIntegration()], - tracesSampleRate: 1.0, - tracePropagationTargets: [/^\//], -}); - -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx deleted file mode 100644 index df56b2a3a8c0..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as Sentry from '@sentry/react-router/cloudflare'; -import { isbot } from 'isbot'; -import { renderToReadableStream } from 'react-dom/server'; -import { type EntryContext, type HandleErrorFunction, ServerRouter } from 'react-router'; - -// workerd has no `renderToPipeableStream`, so this renders to a web stream instead. -async function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - routerContext: EntryContext, -): Promise { - let shellRendered = false; - const userAgent = request.headers.get('user-agent'); - - const body = await renderToReadableStream(, { - signal: request.signal, - onError(error: unknown) { - responseStatusCode = 500; - // Errors thrown after the shell has flushed can't change the status code, so surface them. - if (shellRendered) { - // eslint-disable-next-line no-console - console.error(error); - } - }, - }); - shellRendered = true; - - // Bots need complete markup rather than a streamed shell. - if (userAgent && isbot(userAgent)) { - await body.allReady; - } - - responseHeaders.set('Content-Type', 'text/html'); - - return new Response(Sentry.injectTraceMetaTags(body), { - headers: responseHeaders, - status: responseStatusCode, - }); -} - -export const handleError: HandleErrorFunction = (error, { request }) => { - // React Router aborts interrupted requests, don't report those. - if (!request.signal.aborted) { - Sentry.captureException(error); - // eslint-disable-next-line no-console - console.error(error); - } -}; - -export default Sentry.wrapSentryHandleRequest(handleRequest); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx deleted file mode 100644 index c09b53b99d46..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts deleted file mode 100644 index 744ffc9485de..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { index, prefix, route, type RouteConfig } from '@react-router/dev/routes'; - -export default [ - index('routes/home.tsx'), - ...prefix('performance', [route('db-mysql', 'routes/performance/db-mysql.tsx')]), -] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx deleted file mode 100644 index 7e59685f3987..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Link } from 'react-router'; - -export default function Home() { - return ( -
-

react-router-8-cloudflare

- db-mysql -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml deleted file mode 100644 index c03828e36b43..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -services: - db: - image: mysql:8.0 - restart: always - container_name: e2e-tests-react-router-8-cloudflare-mysql - # The `mysql` 2.x driver doesn't speak MySQL 8's default - # `caching_sha2_password` auth, so force the legacy plugin. - command: ['--default-authentication-plugin=mysql_native_password'] - ports: - - '3306:3306' - environment: - MYSQL_ROOT_PASSWORD: docker - healthcheck: - test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker'] - interval: 2s - timeout: 3s - retries: 30 - start_period: 10s diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs deleted file mode 100644 index b0695e3731f7..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { execSync } from 'child_process'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default async function globalSetup() { - // `--wait` blocks until the healthcheck passes, so the first request can connect. - execSync('docker compose up -d --wait', { - cwd: __dirname, - stdio: 'inherit', - }); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs deleted file mode 100644 index 2742279431ad..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { execSync } from 'child_process'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -export default async function globalTeardown() { - execSync('docker compose down --volumes', { - cwd: __dirname, - stdio: 'inherit', - }); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json deleted file mode 100644 index 094e2e4ebfbe..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "react-router-8-cloudflare", - "version": "0.1.0", - "type": "module", - "private": true, - "dependencies": { - "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", - "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "isbot": "^5.1.17", - "mysql": "^2.18.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-router": "^8" - }, - "devDependencies": { - "@cloudflare/vite-plugin": "^1.35.0", - "@cloudflare/workers-types": "^4.20260504.0", - "@playwright/test": "~1.63.0", - "@react-router/dev": "^8", - "@sentry-internal/test-utils": "link:../../../test-utils", - "@types/mysql": "^2.15.26", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "typescript": "^5.9.0", - "vite": "7.3.5", - "wrangler": "^4.120.0" - }, - "scripts": { - "build": "react-router build", - "dev": "react-router dev", - "preview": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030", - "proxy": "node start-event-proxy.mjs", - "typecheck": "react-router typegen && tsc", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm typecheck && TEST_ENV=production playwright test" - }, - "volta": { - "node": "24.15.0", - "extends": "../../package.json" - }, - "sentryTest": { - "optional": true - } -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs deleted file mode 100644 index 579222812424..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; - -const config = getPlaywrightConfig( - { - startCommand: 'pnpm preview', - port: 3030, - }, - { - globalSetup: './global-setup.mjs', - globalTeardown: './global-teardown.mjs', - }, -); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico deleted file mode 100644 index 5dbdfcddcb14182535f6d32d1c900681321b1aa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts deleted file mode 100644 index 51e8967770b3..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Config } from '@react-router/dev/config'; - -export default { - ssr: true, -} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs deleted file mode 100644 index d78c58fa1714..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'react-router-8-cloudflare', -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts deleted file mode 100644 index 1dfa920d1625..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; - -// Under span streaming the mysql span name is the query summary, so both queries here are named -// `SELECT`. `db.query.text` is what tells them apart. -test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { - const spansPromise = collectStreamedSpans( - 'react-router-8-cloudflare', - spansOfTrace => - spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'db'), - ); - - const res = await request.get('/performance/db-mysql'); - expect(res.status()).toBe(200); - - const spans = await spansPromise; - const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); - expect(firstQuery).toBeDefined(); - expect(firstQuery!.name).toBe('SELECT'); - expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); - expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); - expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); - expect(firstQuery!.attributes['server.port']?.value).toBe(3306); - expect(firstQuery!.attributes['db.user']?.value).toBe('root'); -}); - -test('a nested query lands on the same segment (async context restored)', async ({ request }) => { - const spansPromise = collectStreamedSpans( - 'react-router-8-cloudflare', - spansOfTrace => - spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && - spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, - ); - - const res = await request.get('/performance/db-mysql'); - expect(res.status()).toBe(200); - - const spans = await spansPromise; - - // These are scoped to the http.server segment's trace, so both queries landing here is what proves - // the nested one kept the async context. - const queryTexts = spans - .filter(span => getSpanOp(span) === 'db') - .map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT ? + ? AS solution'); - expect(queryTexts).toContain('SELECT NOW()'); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json deleted file mode 100644 index 573bcd3ad911..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - // workers-types rather than node: the server runs in workerd. - "types": ["@cloudflare/workers-types", "vite/client"], - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", - - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true - }, - "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"] -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts deleted file mode 100644 index cd07d7a3fe03..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { cloudflare } from '@cloudflare/vite-plugin'; -import { reactRouter } from '@react-router/dev/vite'; -import { sentryReactRouter } from '@sentry/react-router/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig(async config => ({ - plugins: [ - cloudflare({ viteEnvironment: { name: 'ssr' } }), - reactRouter(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...((await sentryReactRouter({ sourcemaps: { disable: true } }, config)) as any[]), - ], -})); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts deleted file mode 100644 index d9a6c5451d72..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Sentry from '@sentry/cloudflare'; -import { createRequestHandler } from 'react-router'; - -const requestHandler = createRequestHandler(() => import('virtual:react-router/server-build'), import.meta.env.MODE); - -interface Env { - E2E_TEST_DSN: string; -} - -// `withSentry` is what reads the build-time orchestrion marker; without it the injected -// `diagnostics_channel` publishers would fire with nobody subscribed. -export default Sentry.withSentry( - (env: Env) => ({ - dsn: env.E2E_TEST_DSN, - tunnel: 'http://localhost:3031/', - tracesSampleRate: 1.0, - environment: 'qa', // dynamic sampling bias to keep transactions - }), - { - // No load context: React Router 8 takes a `RouterContextProvider`, not v7's `{ cloudflare }` - // object, and nothing here reads bindings from a loader. - async fetch(request) { - return requestHandler(request); - }, - } satisfies ExportedHandler, -); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc deleted file mode 100644 index 32521671d64e..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "node_modules/wrangler/config-schema.json", - "name": "react-router-8-cloudflare", - "compatibility_date": "2026-06-29", - "compatibility_flags": ["nodejs_compat"], - "main": "./workers/app.ts", - "assets": { - "directory": "./build/client", - }, - "observability": { - "enabled": true, - }, -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts index 3e3839295b1d..1e2b1b1dfd10 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts @@ -20,5 +20,6 @@ export default [ route('server-action', 'routes/performance/server-action.tsx'), route('with-middleware', 'routes/performance/with-middleware.tsx'), route('redis', 'routes/performance/redis.tsx'), + route('db-mysql', 'routes/performance/db-mysql.tsx'), ]), ] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx similarity index 87% rename from dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx rename to dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx index ea2ac1fcea61..55d73a44004c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx @@ -1,8 +1,8 @@ import mysql from 'mysql'; import type { Route } from './+types/db-mysql'; -// These queries produce `db` spans from the build-time orchestrion transform alone — workerd can't -// monkey-patch requires, so there's no OTel hook involved. +// On Cloudflare these `db` spans come from the build-time orchestrion transform alone, because workerd +// can't monkey-patch requires. export async function loader(): Promise<{ status: string }> { // Connect inside the loader: workerd forbids I/O in global scope. const connection = mysql.createConnection({ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml index 27367c5ae12a..dfb0c2de26c0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml @@ -10,3 +10,20 @@ services: interval: 1s timeout: 3s retries: 30 + mysql: + image: mysql:8.0 + restart: always + container_name: e2e-tests-react-router-8-mysql + # The `mysql` 2.x driver doesn't speak MySQL 8's default + # `caching_sha2_password` auth, so force the legacy plugin. + command: ['--default-authentication-plugin=mysql_native_password'] + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: docker + healthcheck: + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 10s diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs index 55af1feaa14c..a7ab404749f5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs @@ -13,7 +13,7 @@ export default async function globalSetup() { // part of the same project - but the container name is fixed, so the daemon // still refuses to create a new one. Force-remove any stale leftover first. try { - execSync('docker rm -f e2e-tests-react-router-8-redis', { stdio: 'ignore' }); + execSync('docker rm -f e2e-tests-react-router-8-redis e2e-tests-react-router-8-mysql', { stdio: 'ignore' }); } catch { // no stale container to remove } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json index a89e3ef6bc7f..7f22e8ce10f1 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json @@ -14,10 +14,12 @@ "@sentry/deno": "file:../../packed/sentry-deno-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "ioredis": "^5.11.1", - "isbot": "^5.1.43" + "isbot": "^5.1.43", + "mysql": "^2.18.1" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.35.0", + "@types/mysql": "^2.15.26", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/node": "^22", diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts new file mode 100644 index 000000000000..20377d10c47d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +// Under span streaming the mysql span name is the query summary, so both queries here are named +// `SELECT`. `db.query.text` is what tells them apart. +test.describe('server - mysql db spans', () => { + test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'db'), + ); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); + + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); + expect(firstQuery).toBeDefined(); + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); + }); + + test('a nested query lands on the same segment (async context restored)', async ({ request }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, + ); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const spans = await spansPromise; + + // These are scoped to the http.server segment's trace, so both queries landing here is what proves + // the nested one kept the async context. + const queryTexts = spans + .filter(span => getSpanOp(span) === 'db') + .map(span => span.attributes['db.query.text']?.value); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); + expect(queryTexts).toContain('SELECT NOW()'); + }); +}); From 85e545529b3a4f67535fa1756fc2d7b3042c634c Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 10:45:40 +0200 Subject: [PATCH 07/73] ref(core): Use SENTRY_OP convention constant (#24982) Uses `SENTRY_OP` from `@sentry/conventions/attributes` throughout SDK instrumentation and tests. It is an exact replacement for `sentry.op`; emitted values, attribute precedence, and serialization remain unchanged. Deprecates the existing core constant while preserving its public export and SDK re-exports for backward compatibility. Part of #24961 / [JS-3868](https://linear.app/getsentry/issue/JS-3868). --------- Co-authored-by: GPT-6 --- .../public-api/startSpan/streamed/test.ts | 12 +-- .../test.ts | 6 +- .../long-animation-frame-enabled/test.ts | 7 +- .../test.ts | 6 +- .../test.ts | 7 +- .../navigation-aborting-pageload/test.ts | 12 +-- .../multiple-redirects/test.ts | 10 +-- .../pageload-redirect/test.ts | 10 +-- .../navigation-streamed/test.ts | 14 ++-- .../navigation/test.ts | 16 ++-- .../pageload-streamed/test.ts | 10 +-- .../pageload-update-txn-name/test.ts | 10 +-- .../pageload-updateSpanName/test.ts | 10 +-- .../pageload/test.ts | 10 +-- .../reportPageLoaded-streamed/default/test.ts | 10 +-- .../finalTimeout/test.ts | 10 +-- .../navigation/test.ts | 10 +-- .../reportPageLoaded/default/test.ts | 10 +-- .../reportPageLoaded/finalTimeout/test.ts | 10 +-- .../reportPageLoaded/navigation/test.ts | 10 +-- .../suites/tracing/interactions/spans/test.ts | 7 +- .../metrics/pageload-resource-spans/test.ts | 7 +- .../suites/public-api/startSpan/test.ts | 14 ++-- .../suites/tracing/d1/test.ts | 5 +- .../suites/tracing/durableobject-sql/test.ts | 5 +- .../tracing/durableobject-sync-kv/test.ts | 5 +- .../suites/tracing/scheduled/test.ts | 10 +-- .../suites/tracing/workflow/test.ts | 10 +-- .../test-applications/angular-19/package.json | 1 + .../angular-19/tests/performance.test.ts | 13 ++-- .../nuxt-4-static/package.json | 1 + .../nuxt-4-static/tests/cache.test.ts | 13 ++-- .../tests/storage-aliases.test.ts | 13 ++-- .../nuxt-4-static/tests/storage.test.ts | 19 ++--- .../tests/tracing.server.test.ts | 5 +- .../solidstart-static/package.json | 1 + .../tests/performance.server.test.ts | 7 +- .../suites/fs-instrumentation/test.ts | 45 ++++++------ .../startSpan/basic-usage-streamed/test.ts | 12 +-- .../transaction-route/scenario.ts | 4 +- .../transaction-url/scenario.ts | 4 +- .../suites/tracing/groq/test.ts | 7 +- .../ignoreSpans-streamed/children/test.ts | 6 +- .../suites/tracing/langgraph/test.ts | 21 +++--- .../suites/tracing/mistral/test.ts | 9 ++- .../suites/tracing/mysql/test.ts | 7 +- .../tracing/openai/openai-tool-calls/test.ts | 19 ++--- .../suites/tracing/openai/test.ts | 73 ++++++++++--------- .../suites/tracing/openai/v6/test.ts | 43 +++++------ .../suites/tracing/openai/v7/test.ts | 9 ++- .../suites/tracing/postgres-streamed/test.ts | 5 +- .../suites/tracing/redis/test.ts | 8 +- .../suites/tracing/together-ai/test.ts | 7 +- .../vercelai/span-streaming-v4/test.ts | 33 +++++---- .../vercelai/span-streaming-v6/test.ts | 33 +++++---- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + .../browser-utils/src/web-vitals/emitSpan.ts | 14 +--- .../browser-utils/src/web-vitals/softNavs.ts | 6 +- .../browser-utils/src/web-vitals/spans.ts | 7 +- .../test/performance/browserMetrics.test.ts | 14 ++-- packages/browser/src/exports.ts | 1 + .../test/integrations/httpcontext.test.ts | 23 +++--- .../tracing/browserTracingIntegration.test.ts | 25 +++---- packages/bun/src/index.ts | 1 + .../test/integrations/bunHttpServer.test.ts | 8 +- packages/cloudflare/src/index.ts | 1 + packages/core/src/semanticAttributes.ts | 1 + packages/core/src/tracing/sentrySpan.ts | 15 ++-- packages/core/src/tracing/trace.ts | 8 +- packages/core/src/utils/spanUtils.ts | 10 +-- .../lib/tracing/spans/captureSpan.test.ts | 10 +-- packages/core/test/lib/tracing/trace.test.ts | 11 ++- .../core/test/lib/utils/spanUtils.test.ts | 17 ++--- packages/deno/src/index.ts | 1 + packages/effect/test/tracer.test.ts | 11 +-- packages/elysia/src/index.ts | 1 + packages/elysia/src/withElysia.ts | 5 +- packages/google-cloud-serverless/src/index.ts | 1 + packages/nestjs/src/decorators.ts | 6 +- packages/nestjs/test/decorators.test.ts | 10 +-- .../src/common/utils/liveRootSpanAdapter.ts | 6 +- packages/nextjs/src/edge/index.ts | 3 +- .../test/common/liveRootSpanAdapter.test.ts | 6 +- .../enhanceHandleRequestRootSpan.test.ts | 10 +-- packages/node/src/index.ts | 1 + .../fs/vendored/instrumentation.ts | 5 +- .../hooks/updateRouteBeforeResponse.test.ts | 6 +- .../hooks/wrapMiddlewareHandler.test.ts | 11 +-- packages/opentelemetry/test/trace.test.ts | 7 +- packages/react/src/reactrouter.tsx | 3 +- packages/react/test/react-router.test.tsx | 14 +--- .../test/reactrouter-cross-usage.test.tsx | 24 +++--- .../reactrouter-descendant-routes.test.tsx | 20 ++--- packages/react/test/reactrouterv3.test.tsx | 20 ++--- packages/react/test/reactrouterv4.test.tsx | 28 +++---- packages/react/test/reactrouterv5.test.tsx | 28 +++---- packages/react/test/reactrouterv6.test.tsx | 52 ++++++------- packages/remix/src/cloudflare/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/server-utils/src/ai/mastra/index.ts | 5 +- .../graphql/graphql-dc-subscriber.ts | 16 ++-- .../src/integrations/graphql/resolvers.ts | 4 +- .../src/integrations/graphql/spans.ts | 10 ++- .../src/integrations/redis/redis-cache.ts | 14 +--- .../test/ai/lib/tracing/workers-ai.test.ts | 6 +- packages/solid/src/solidrouter.ts | 3 +- packages/solid/test/solidrouter.test.tsx | 18 ++--- packages/solidstart/src/server/index.ts | 1 + .../test/client/solidrouter.test.tsx | 18 ++--- .../withServerActionInstrumentation.test.ts | 9 +-- packages/sveltekit/src/server/index.ts | 1 + packages/sveltekit/src/worker/index.ts | 1 + packages/sveltekit/test/client/load.test.ts | 12 +-- .../integrations/svelteKitSpans.test.ts | 45 ++++++------ .../sveltekit/test/server-common/load.test.ts | 18 ++--- .../test/server-common/serverRoute.test.ts | 15 ++-- packages/vercel-edge/src/index.ts | 1 + 118 files changed, 605 insertions(+), 728 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts index 289a5716746f..e10826f486a9 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -1,10 +1,5 @@ import { expect } from '@playwright/test'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; @@ -19,6 +14,7 @@ import { SENTRY_STATUS_MESSAGE, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; sentryTest( @@ -77,7 +73,7 @@ sentryTest( { attributes: { 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test-child', }, @@ -240,7 +236,7 @@ sentryTest( type: 'string', value: expect.any(String), }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts index 6f7116bd33a7..5f15f4354416 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts @@ -1,7 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; @@ -45,7 +45,7 @@ sentryTest( value: 'https://sentry-test-site.example/path/to/script.js', }, 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), @@ -94,7 +94,7 @@ sentryTest('captures long animation frame span for event listener.', async ({ br 'browser.script.invoker': { type: 'string', value: 'BUTTON#clickme.onclick' }, 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts index 17db0254533e..a6ed74270ebb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -44,7 +45,7 @@ sentryTest( 'browser.script.source_char_position': 0, 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', 'browser.script.invoker_type': 'classic-script', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_OP]: 'ui.long_animation_frame', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), @@ -98,7 +99,7 @@ sentryTest( 'browser.script.invoker': 'BUTTON#clickme.onclick', 'browser.script.invoker_type': 'event-listener', 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_OP]: 'ui.long_animation_frame', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts index d97eba7c9a0c..4a4cda50b962 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts @@ -1,7 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; @@ -47,7 +47,7 @@ sentryTest( value: 'https://sentry-test-site.example/path/to/script.js', }, 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), @@ -96,7 +96,7 @@ sentryTest('captures long animation frame span for event listener.', async ({ br 'browser.script.invoker': { type: 'string', value: 'BUTTON#clickme.onclick' }, 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, + [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts index 4e14a8add77d..5efc2258f48f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -46,7 +47,7 @@ sentryTest( 'browser.script.source_char_position': 0, 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', 'browser.script.invoker_type': 'classic-script', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_OP]: 'ui.long_animation_frame', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), @@ -100,7 +101,7 @@ sentryTest( 'browser.script.invoker': 'BUTTON#clickme.onclick', 'browser.script.invoker_type': 'event-listener', 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.long_animation_frame', + [SENTRY_OP]: 'ui.long_animation_frame', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts index 4791b010015b..f58116dd9c32 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; @@ -47,14 +43,14 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(pageloadRequest.request).toEqual({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts index 8b512db6a4f9..9fd5b0cdc35f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts @@ -1,12 +1,8 @@ import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; sentryTest( 'creates a pageload and navigation root spans each with multiple navigation.redirect childspans', @@ -34,7 +30,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts index b0d8a061fec8..0735f69f127c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts @@ -1,12 +1,8 @@ import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; sentryTest('creates a pageload root span with navigation.redirect childspan', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -27,7 +23,7 @@ sentryTest('creates a pageload root span with navigation.redirect childspan', as [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts index 5d298cb1e9cd..b2281d6ddfa4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts @@ -1,10 +1,5 @@ import { expect } from '@playwright/test'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_TRACE_LIFECYCLE, @@ -13,6 +8,7 @@ import { USER_AGENT_ORIGINAL, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -124,7 +120,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow type: 'string', value: 'idleTimeout', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'navigation', }, @@ -236,7 +232,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: 'url', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'navigation', }, @@ -265,7 +261,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: 'url', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'navigation', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts index 30a457268d45..a7cf07af6838 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -1,11 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -44,14 +40,14 @@ sentryTest('should create a navigation transaction on page navigation', async ({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(pageloadRequest.request).toEqual({ @@ -122,7 +118,7 @@ sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, pa [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest.request).toEqual({ @@ -142,7 +138,7 @@ sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, pa [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest2.request).toEqual({ diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts index 1fdaaadce2bd..e7127322f2a8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts @@ -1,10 +1,5 @@ import { expect } from '@playwright/test'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { BROWSER_NAVIGATION_TYPE, SENTRY_SEGMENT_NAME_SOURCE, @@ -18,6 +13,7 @@ import { USER_AGENT_ORIGINAL, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -136,7 +132,7 @@ sentryTest( type: 'string', value: 'idleTimeout', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'pageload', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts index 401b30e2a5fb..5bb181132068 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -30,7 +26,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }); expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts index f6c3282e30c8..3a3952d1637b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -28,7 +24,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }); expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts index bd02b4799199..b1aa7b48bb0d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -29,7 +25,7 @@ sentryTest('creates a pageload transaction with url as source', async ({ getLoca [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts index 3d3d4e69069c..b5f733ff6298 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -28,7 +24,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'reportPageLoaded' }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts index 9983809f9947..d7da4c7c3cc6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -28,7 +24,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, 'sentry.idle_span_finish_reason': { type: 'string', value: 'finalTimeout' }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts index e71f12c00993..b224fa51fdb3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -28,7 +24,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'pageload' }, + [SENTRY_OP]: { type: 'string', value: 'pageload' }, 'sentry.idle_span_finish_reason': { type: 'string', value: 'cancelled' }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts index 9c8c0b48fb69..8448aad75624 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -30,7 +26,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'reportPageLoaded', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts index 90deb74d0f2b..0bcc574b5933 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -30,7 +26,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'finalTimeout', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts index 6b0bf8ceb8b8..c3de6c158f9c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -30,7 +26,7 @@ sentryTest( [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts index 2a6123aa2aa9..15cf43e68b5e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_ID, @@ -12,6 +12,7 @@ import { URL_PATH, SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -76,7 +77,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'idleTimeout', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'ui.action.click', }, @@ -133,7 +134,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'stream', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'ui.interaction.click', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 12e3861db87f..3b8f662f0003 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -95,7 +96,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img', + [SENTRY_OP]: 'resource.img', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'sentry-test-site.example', 'url.domain': 'sentry-test-site.example', @@ -148,7 +149,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link', + [SENTRY_OP]: 'resource.link', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'sentry-test-site.example', 'url.domain': 'sentry-test-site.example', diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts index f5a787bbc91e..00d89567b577 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts @@ -1,9 +1,4 @@ -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_SDK_NAME, @@ -14,6 +9,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; @@ -80,7 +76,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test-child', }, @@ -167,7 +163,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, + [SENTRY_OP]: { type: 'string', value: 'test' }, [SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, @@ -200,7 +196,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.cloudflare' }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'http.server' }, + [SENTRY_OP]: { type: 'string', value: 'http.server' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'route' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts index 2cd081b06b51..13a6474f8377 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest'; import type { Envelope, SerializedStreamedSpan } from '@sentry/core'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { SENTRY_SDK_NAME, SENTRY_SDK_VERSION, @@ -8,6 +8,7 @@ import { SENTRY_SEGMENT_NAME, SENTRY_TRACE_LIFECYCLE, SENTRY_ENVIRONMENT, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { createRunner } from '../../../runner'; import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; @@ -35,7 +36,7 @@ function commonAttributes(segmentSpan: SerializedStreamedSpan): SerializedStream [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan.span_id }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentSpan.name }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db.query' }, + [SENTRY_OP]: { type: 'string', value: 'db.query' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' }, 'db.system.name': { type: 'string', value: 'cloudflare-d1' }, }; diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts index e9feaa6215f4..3b0fbb839714 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts @@ -1,5 +1,6 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Envelope } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; import { getSpansFromEnvelope } from '../../../spanUtils'; @@ -32,7 +33,7 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal expect.objectContaining({ name: 'CREATE TABLE users', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db.query' }, + [SENTRY_OP]: { type: 'string', value: 'db.query' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object.sql' }, 'db.system.name': { type: 'string', value: 'cloudflare-durable-object-sql' }, 'db.operation.name': { type: 'string', value: 'exec' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts index 1051890b8f94..113d35d64cf8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Envelope } from '@sentry/core'; import { expect, it } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { createRunner } from '../../../runner'; import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; @@ -29,7 +30,7 @@ it('instruments sync KV operations on Durable Object storage', async ({ signal } expect.objectContaining({ name: 'durable_object_storage_kv_put', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'db' }, + [SENTRY_OP]: { type: 'string', value: 'db' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object' }, 'db.system.name': { type: 'string', value: 'cloudflare-durable-object-sql' }, 'db.operation.name': { type: 'string', value: 'put' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts index 6cb7888217b0..637dddd01aa3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts @@ -1,11 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; import type { Event } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { createRunner } from '../../../runner'; import { getSpansFromEnvelope } from '../../../spanUtils'; @@ -24,7 +20,7 @@ it('Scheduled handler creates a segment span with correct attributes', async ({ is_segment: true, status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'function' }, + [SENTRY_OP]: { type: 'string', value: 'function' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.scheduled' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'task' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts index f54038aaa714..5934cf064a1e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts @@ -1,10 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { createRunner } from '../../../runner'; it('Workflow steps create segment spans with correct attributes', async ({ signal }) => { @@ -28,7 +24,7 @@ it('Workflow steps create segment spans with correct attributes', async ({ signa is_segment: true, status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'function' }, + [SENTRY_OP]: { type: 'string', value: 'function' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.workflow' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'task' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json index e3d141736b9d..417939441749 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -36,6 +36,7 @@ "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", "@playwright/test": "~1.63.0", + "@sentry/conventions": "0.25.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/jasmine": "~5.1.0", diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts index 2abfce92b545..a9e2b94e214e 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -1,7 +1,8 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; // Cannot use @sentry/angular here due to build stuff -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; // The `angular-19 (streamed)` variant builds the app with `traceLifecycle: 'stream'`, which emits // spans instead of transactions. See `streamed-performance.test.ts` for that variant. @@ -265,7 +266,7 @@ test.describe('TraceDirective', () => { expect.arrayContaining([ expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.mount', + [SENTRY_OP]: 'ui.mount', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', 'ui.component_name': 'sample-component', }, @@ -277,7 +278,7 @@ test.describe('TraceDirective', () => { }), expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.mount', + [SENTRY_OP]: 'ui.mount', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', 'ui.component_name': 'app-sample-component', }, @@ -311,7 +312,7 @@ test.describe('TraceClass Decorator', () => { expect(classDecoratorSpan).toEqual( expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.mount', + [SENTRY_OP]: 'ui.mount', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', 'ui.component_name': 'ComponentTrackingComponent', }, @@ -344,7 +345,7 @@ test.describe('TraceMethod Decorator', () => { expect(ngInitSpan).toEqual( expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', 'code.function.name': 'ngOnInit', }, @@ -375,7 +376,7 @@ test.describe('TraceMethod Decorator', () => { expect(ngAfterViewInitSpan).toEqual( expect.objectContaining({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', 'code.function.name': 'ngAfterViewInit', }, diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json index e77ada5ee0d9..fc4a77a4cedc 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@playwright/test": "~1.63.0", + "@sentry/conventions": "0.25.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts index d8ad57bca377..677322ccae16 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Cache Instrumentation', () => { const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; @@ -42,7 +43,7 @@ test.describe('Cache Instrumentation', () => { ); if (cacheMissSpan) { expect(cacheMissSpan.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_HIT]: false, 'db.operation.name': 'getItem', @@ -59,7 +60,7 @@ test.describe('Cache Instrumentation', () => { ); if (cacheHitSpan) { expect(cacheHitSpan.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', @@ -78,7 +79,7 @@ test.describe('Cache Instrumentation', () => { ); if (cacheSetSpan) { expect(cacheSetSpan.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', + [SENTRY_OP]: 'cache.put', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'setItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), @@ -133,10 +134,10 @@ test.describe('Cache Instrumentation', () => { expect(allCacheSpans?.length).toBeGreaterThan(0); // Get all getItem operations - const allGetItemSpans = allCacheSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.get'); + const allGetItemSpans = allCacheSpans?.filter(span => span.data?.[SENTRY_OP] === 'cache.get'); // Get all setItem operations - const allSetItemSpans = allCacheSpans?.filter(span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'cache.put'); + const allSetItemSpans = allCacheSpans?.filter(span => span.data?.[SENTRY_OP] === 'cache.put'); // We should have both get and set operations expect(allGetItemSpans?.length).toBeGreaterThan(0); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts index ee1950050482..2d56c4cd5449 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation - Aliases', () => { const prefixKey = (key: string) => `test-storage:${key}`; @@ -28,7 +29,7 @@ test.describe('Storage Instrumentation - Aliases', () => { const setSpan = setSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); expect(setSpan).toBeDefined(); expect(setSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', + [SENTRY_OP]: 'cache.put', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), 'db.operation.name': 'setItem', @@ -43,7 +44,7 @@ test.describe('Storage Instrumentation - Aliases', () => { const getSpan = getSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); expect(getSpan).toBeDefined(); expect(getSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, @@ -59,7 +60,7 @@ test.describe('Storage Instrumentation - Aliases', () => { const hasSpan = hasSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:user')); expect(hasSpan).toBeDefined(); expect(hasSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, @@ -75,7 +76,7 @@ test.describe('Storage Instrumentation - Aliases', () => { const delSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp1')); expect(delSpan).toBeDefined(); expect(delSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', + [SENTRY_OP]: 'cache.remove', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp1'), 'db.operation.name': 'removeItem', @@ -87,7 +88,7 @@ test.describe('Storage Instrumentation - Aliases', () => { const removeSpan = removeSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('alias:temp2')); expect(removeSpan).toBeDefined(); expect(removeSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', + [SENTRY_OP]: 'cache.remove', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp2'), 'db.operation.name': 'removeItem', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts index b4138a8eb6e8..e16a0213c0a2 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation', () => { const prefixKey = (key: string) => `test-storage:${key}`; @@ -28,7 +29,7 @@ test.describe('Storage Instrumentation', () => { const setItemSpan = setItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); expect(setItemSpan).toBeDefined(); expect(setItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', + [SENTRY_OP]: 'cache.put', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), 'db.operation.name': 'setItem', @@ -45,7 +46,7 @@ test.describe('Storage Instrumentation', () => { ); expect(setItemRawSpan).toBeDefined(); expect(setItemRawSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.put', + [SENTRY_OP]: 'cache.put', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), 'db.operation.name': 'setItemRaw', @@ -59,7 +60,7 @@ test.describe('Storage Instrumentation', () => { const hasItemSpan = hasItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); expect(hasItemSpan).toBeDefined(); expect(hasItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, @@ -74,7 +75,7 @@ test.describe('Storage Instrumentation', () => { const getItemSpan = getItemSpans.find(span => span.data?.[SEMANTIC_ATTRIBUTE_CACHE_KEY] === prefixKey('user:123')); expect(getItemSpan).toBeDefined(); expect(getItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, @@ -92,7 +93,7 @@ test.describe('Storage Instrumentation', () => { ); expect(getItemRawSpan).toBeDefined(); expect(getItemRawSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, @@ -105,7 +106,7 @@ test.describe('Storage Instrumentation', () => { const getKeysSpans = findSpansByMethod('getKeys'); expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); expect(getKeysSpans[0]?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.get', + [SENTRY_OP]: 'cache.get', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'getKeys', 'db.collection.name': 'test-storage', @@ -120,7 +121,7 @@ test.describe('Storage Instrumentation', () => { ); expect(removeItemSpan).toBeDefined(); expect(removeItemSpan?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', + [SENTRY_OP]: 'cache.remove', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('batch:1'), 'db.operation.name': 'removeItem', @@ -132,7 +133,7 @@ test.describe('Storage Instrumentation', () => { const clearSpans = findSpansByMethod('clear'); expect(clearSpans.length).toBeGreaterThanOrEqual(1); expect(clearSpans[0]?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'cache.remove', + [SENTRY_OP]: 'cache.remove', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'clear', 'db.collection.name': 'test-storage', diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts index 2bc3c805f172..1a2dc1744613 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test('sends a server action transaction on pageload', async ({ page }) => { const transactionPromise = waitForTransaction('nuxt-4-static', transactionEvent => { @@ -14,7 +15,7 @@ test('sends a server action transaction on pageload', async ({ page }) => { expect(transaction.contexts.trace).toEqual( expect.objectContaining({ data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server', }), }), diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json index ebc880b29186..2d4607681775 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@playwright/test": "~1.63.0", + "@sentry/conventions": "0.25.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts index 7d15bbe3eba8..c593a0fd396d 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts @@ -1,6 +1,7 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/solidstart'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/solidstart'; test('sends a server action transaction on pageload', async ({ page }) => { const transactionPromise = waitForTransaction('solidstart-static', transactionEvent => { @@ -16,7 +17,7 @@ test('sends a server action transaction on pageload', async ({ page }) => { expect.objectContaining({ description: 'getPrefecture', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', 'code.function.name': 'getPrefecture', }, @@ -41,7 +42,7 @@ test('sends a server action transaction on client navigation', async ({ page }) expect.objectContaining({ description: 'getPrefecture', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', 'code.function.name': 'getPrefecture', }, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts index a49710d6e560..53d79a85a603 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts @@ -1,4 +1,5 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; +import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../utils/runner'; @@ -26,7 +27,7 @@ describe('fs instrumentation', () => { data: { 'error.type': 'ENOENT', path_argument: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -52,7 +53,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { path_argument: expect.stringMatching('/fixtures/some-file.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -62,7 +63,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { path_argument: expect.stringMatching('/fixtures/some-file-promises.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -72,7 +73,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -99,7 +100,7 @@ describe('fs instrumentation', () => { data: { src_argument: expect.stringMatching('/fixtures/some-file.txt'), dest_argument: expect.stringMatching('/fixtures/some-file.txt.copy'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -110,7 +111,7 @@ describe('fs instrumentation', () => { data: { src_argument: expect.stringMatching('/fixtures/some-file-promises.txt'), dest_argument: expect.stringMatching('/fixtures/some-file-promises.txt.copy'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -121,7 +122,7 @@ describe('fs instrumentation', () => { data: { src_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), dest_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.copy'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -148,7 +149,7 @@ describe('fs instrumentation', () => { data: { existing_path_argument: expect.stringMatching('/fixtures/some-file.txt'), new_path_argument: expect.stringMatching('/fixtures/some-file.txt.link'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -159,7 +160,7 @@ describe('fs instrumentation', () => { data: { existing_path_argument: expect.stringMatching('/some-file-promises.txt'), new_path_argument: expect.stringMatching('/some-file-promises.txt.link'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -170,7 +171,7 @@ describe('fs instrumentation', () => { data: { existing_path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), new_path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -196,7 +197,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { prefix_argument: expect.stringMatching('/foo-'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -206,7 +207,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { prefix_argument: expect.stringMatching('/foo-'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -216,7 +217,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { prefix_argument: expect.stringMatching('/foo-'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -243,7 +244,7 @@ describe('fs instrumentation', () => { data: { target_argument: expect.stringMatching('/some-file-promisify.txt'), path_argument: expect.stringMatching('/some-file-promisify.txt.symlink'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -254,7 +255,7 @@ describe('fs instrumentation', () => { data: { target_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -265,7 +266,7 @@ describe('fs instrumentation', () => { data: { target_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -291,7 +292,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { path_argument: expect.stringMatching('/fixtures/some-file.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -301,7 +302,7 @@ describe('fs instrumentation', () => { status: 'ok', data: { path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -338,7 +339,7 @@ describe('fs instrumentation', () => { // `path_argument` is recorded, but `error.type` is NOT, since `recordErrorMessagesAsSpanAttributes` is off data: { path_argument: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -375,7 +376,7 @@ describe('fs instrumentation', () => { // `error.type` is recorded, but `path_argument` is NOT, since `recordFilePaths` is off data: { 'error.type': 'ENOENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), @@ -401,7 +402,7 @@ describe('fs instrumentation', () => { status: 'ok', // Neither `path_argument` nor `error.type` are recorded data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', + [SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', }, }), diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index 2eb452d43386..dd7d18f7c961 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -1,9 +1,4 @@ -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_SEGMENT_ID, @@ -14,6 +9,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { expect, test } from 'vitest'; import { createRunner } from '../../../../utils/runner'; @@ -60,7 +56,7 @@ test('sends a streamed span envelope with correct spans for a manually started s attributes: { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, 'sentry.is_localhost': { type: 'boolean', value: false }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'test-child', }, @@ -146,7 +142,7 @@ test('sends a streamed span envelope with correct spans for a manually started s const expectedAttributes: Record = { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, 'sentry.is_localhost': { type: 'boolean', value: false }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, + [SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SDK_NAME]: { type: 'string', value: EXPECTED_SDK_NAME }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts index 4d67fee0a0d2..83f987a0cfdf 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts @@ -1,4 +1,4 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; @@ -17,7 +17,7 @@ Sentry.startSpan( attributes: { 'http.request.method': 'GET', 'http.route': '/route', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', }, }, diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts index 69b76f248555..4c46fe4bcd9e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts @@ -1,4 +1,4 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; @@ -17,7 +17,7 @@ Sentry.startSpan( attributes: { 'http.request.method': 'GET', 'http.route': '/route', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }, diff --git a/dev-packages/node-integration-tests/suites/tracing/groq/test.ts b/dev-packages/node-integration-tests/suites/tracing/groq/test.ts index 5797f38d2d1b..d63eca3f15c5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/groq/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/groq/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -15,6 +15,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -37,7 +38,7 @@ describe('Groq integration', () => { expect(chatSpan!.name).toBe('chat llama-3.3-70b-versatile'); expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('llama-3.3-70b-versatile'); @@ -70,7 +71,7 @@ describe('Groq integration', () => { expect(embeddingsSpan).toBeDefined(); expect(embeddingsSpan!.name).toBe('embeddings nomic-embed-text-v1_5'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.embeddings'); + expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); expect(embeddingsSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT]).toBeUndefined(); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts index c1ac1f7fc4a6..fefa45353102 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/test.ts @@ -1,6 +1,6 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; describe('filtering child spans with ignoreSpans (streaming)', () => { afterAll(() => { @@ -28,9 +28,7 @@ describe('filtering child spans with ignoreSpans (streaming)', () => { // Would be 7 if we didn't ignore the 'middleware - expressInit' and 'custom-to-drop' spans expect(container.items).toHaveLength(8); const getSpan = (name: string, op: string) => - container.items.find( - item => item.name === name && item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === op, - ); + container.items.find(item => item.name === name && item.attributes[SENTRY_OP]?.value === op); const queryMiddlewareSpan = getSpan('query', 'middleware'); const corsMiddlewareSpan = getSpan('corsMiddleware', 'middleware'); const requestHandlerSpan = getSpan('/test/express', 'handler'); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 3f85267b154e..e436b94a61e8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_AGENT_NAME, @@ -15,6 +15,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { getStringAttributeValue } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -274,7 +275,7 @@ describe('LangGraph integration', () => { status: 'ok', attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), [GEN_AI_PIPELINE_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), @@ -284,7 +285,7 @@ describe('LangGraph integration', () => { expect(spans).toContainEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.chat' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), }), }), @@ -314,7 +315,7 @@ describe('LangGraph integration', () => { expect.objectContaining({ status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'math_assistant' }), }), @@ -325,7 +326,7 @@ describe('LangGraph integration', () => { name: 'execute_tool add', status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'add' }), }), @@ -336,15 +337,13 @@ describe('LangGraph integration', () => { name: 'execute_tool multiply', status: 'ok', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), + [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.execute_tool' }), [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'execute_tool' }), [GEN_AI_TOOL_NAME]: expect.objectContaining({ value: 'multiply' }), }), }), ); - expect( - spans.filter(span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat'), - ).toHaveLength(3); + expect(spans.filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')).toHaveLength(3); }, }) .start() @@ -362,9 +361,7 @@ describe('LangGraph integration', () => { }) .expect({ span: container => { - const chatSpans = container.items.filter( - s => s.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat', - ); + const chatSpans = container.items.filter(s => s.attributes[SENTRY_OP]?.value === 'gen_ai.chat'); expect(chatSpans).toHaveLength(1); expect(chatSpans[0]?.attributes[GEN_AI_AGENT_NAME]?.value).toBe('plain_assistant'); }, diff --git a/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts b/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts index 49fe09771da1..4ae126d350d9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_AGENT_NAME, GEN_AI_EMBEDDINGS_INPUT, @@ -20,6 +20,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; @@ -47,7 +48,7 @@ describe('Mistral integration', () => { expect(chatSpan!.name).toBe('chat mistral-small-latest'); expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('mistral-small-latest'); @@ -140,7 +141,7 @@ describe('Mistral integration', () => { expect(embeddingsSpan!.name).toBe('embeddings mistral-embed'); expect(embeddingsSpan!.status).toBe('ok'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.embeddings'); + expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('mistral-embed'); @@ -183,7 +184,7 @@ describe('Mistral integration', () => { expect(agentSpan!.name).toBe('invoke_agent'); expect(agentSpan!.status).toBe('ok'); expect(agentSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); - expect(agentSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.invoke_agent'); + expect(agentSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.invoke_agent'); expect(agentSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(agentSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(agentSpan!.attributes[GEN_AI_AGENT_NAME]?.value).toBe('ag-mock-123'); diff --git a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts index 2188dd49f356..e0db6de0be75 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mysql/test.ts @@ -3,8 +3,7 @@ import { afterAll, beforeAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests, createEsmAndCjsTests } from '../../../utils/runner'; import { startMysqlTestServer } from './mysql-test-server'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; describe('mysql auto instrumentation', () => { // A minimal in-process MySQL server (on a random free port) so the client's @@ -176,9 +175,7 @@ describe('mysql auto instrumentation', () => { const segmentSpan = container.items.find(item => item.is_segment); expect(segmentSpan?.name).toBe('Test Transaction'); - const dbSpans = container.items.filter( - spanItem => spanItem.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'db', - ); + const dbSpans = container.items.filter(spanItem => spanItem.attributes[SENTRY_OP]?.value === 'db'); expect(dbSpans.length).toBe(2); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts index 89bf6c7b0544..cd746332059b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -15,6 +15,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -92,7 +93,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -142,7 +143,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -202,7 +203,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -255,7 +256,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -328,7 +329,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -392,7 +393,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -463,7 +464,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -527,7 +528,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index a27e3308179a..27382085cdcc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { @@ -18,6 +18,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, @@ -50,7 +51,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -107,7 +108,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -157,7 +158,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -184,7 +185,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -247,7 +248,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -319,7 +320,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -340,7 +341,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -393,7 +394,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -466,7 +467,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -535,7 +536,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -593,7 +594,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -624,7 +625,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -699,7 +700,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -783,7 +784,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -804,7 +805,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -866,7 +867,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1019,7 +1020,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1064,7 +1065,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1093,7 +1094,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1140,7 +1141,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1187,7 +1188,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1238,7 +1239,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1271,7 +1272,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1312,7 +1313,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1360,7 +1361,7 @@ describe('OpenAI integration', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', @@ -1402,7 +1403,7 @@ describe('OpenAI integration', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', @@ -1446,7 +1447,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(conversationCreateSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(conversationCreateSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1475,7 +1476,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(conversationResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(conversationResponseSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1498,12 +1499,12 @@ describe('OpenAI integration', () => { const unlinkedResponseSpan = container.items.find( span => - span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat' && + span.attributes[SENTRY_OP]?.value === 'gen_ai.chat' && span.attributes[GEN_AI_CONVERSATION_ID] === undefined, ); expect(unlinkedResponseSpan).toBeDefined(); expect(unlinkedResponseSpan!.status).toBe('ok'); - expect(unlinkedResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(unlinkedResponseSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1518,7 +1519,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(previousResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(previousResponseSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1549,7 +1550,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(rawConversationSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(rawConversationSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1749,7 +1750,7 @@ describe('OpenAI integration', () => { expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); expect(chatCompletionSpan!.status).toBe('ok'); expect(chatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({ type: 'string', value: 'chat' }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1779,7 +1780,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -1809,7 +1810,7 @@ describe('OpenAI integration', () => { expect(parsedResponsesSpan!.name).toBe('chat gpt-4o'); expect(parsedResponsesSpan!.status).toBe('ok'); expect(parsedResponsesSpan!.attributes[GEN_AI_OPERATION_NAME]).toEqual({ type: 'string', value: 'chat' }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index 091867486e4e..be3746e90068 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -16,6 +16,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, @@ -50,7 +51,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -105,7 +106,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -155,7 +156,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -182,7 +183,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -245,7 +246,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -317,7 +318,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -359,7 +360,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -426,7 +427,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -484,7 +485,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -515,7 +516,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -590,7 +591,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -674,7 +675,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'openai', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -784,7 +785,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -829,7 +830,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -858,7 +859,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -924,7 +925,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -975,7 +976,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1008,7 +1009,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -1075,7 +1076,7 @@ describe('OpenAI integration (V6)', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', @@ -1128,7 +1129,7 @@ describe('OpenAI integration (V6)', () => { trace_id: expect.any(String), data: { [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts index 6d7eb552c219..646b63ec7a6a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { GEN_AI_OPERATION_NAME, @@ -11,6 +11,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { conditionalTest } from '../../../../utils/index'; @@ -44,7 +45,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { expect(chatCompletionSpan).toBeDefined(); expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); expect(chatCompletionSpan!.status).toBe('ok'); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); @@ -135,7 +136,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { span => span.name === 'embeddings text-embedding-3-small' && span.status === 'ok', ); expect(singleEmbeddingSpan).toBeDefined(); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); @@ -184,7 +185,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { expect(chatCompletionSpan).toBeDefined(); expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); expect(chatCompletionSpan!.status).toBe('ok'); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts index 3a2cea4a4c74..bec779c70b02 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres-streamed/test.ts @@ -1,6 +1,5 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; +import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { conditionalTest, EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -157,7 +156,7 @@ const CREATE_NATIVE_USER_TABLE_STATEMENT = 'CREATE TABLE "NativeUser" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))'; function getDbSpans(container: SerializedStreamedSpanContainer): SerializedStreamedSpanContainer['items'] { - return container.items.filter(item => item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'db'); + return container.items.filter(item => item.attributes[SENTRY_OP]?.value === 'db'); } describeWithDockerCompose('postgres auto instrumentation (streamed)', { workingDirectory: [__dirname] }, () => { diff --git a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts index 50cd473957b8..d8b8b728a2af 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis/test.ts @@ -1,5 +1,5 @@ -import { SENTRY_TRACE_LIFECYCLE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, type SerializedStreamedSpanContainer } from '@sentry/core'; +import { SENTRY_TRACE_LIFECYCLE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { type SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { EXPECTED_SDK_NAME } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests, describeWithDockerCompose } from '../../../utils/runner'; @@ -136,9 +136,7 @@ describeWithDockerCompose('redis auto instrumentation', { workingDirectory: [__d const segmentSpan = container.items.find(item => item.is_segment); expect(segmentSpan?.name).toBe('Test Span'); - const dbSpans = container.items.filter( - item => item.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === redisSpanOp, - ); + const dbSpans = container.items.filter(item => item.attributes[SENTRY_OP]?.value === redisSpanOp); expect(dbSpans).toEqual([ expectedDbSpan({ operation: 'set', statement: 'set test-key [1 other arguments]' }), diff --git a/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts b/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts index 2e7c3b94dc7b..b9a7fb1fe38b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -15,6 +15,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -37,7 +38,7 @@ describe('Together integration', () => { expect(chatSpan!.name).toBe('chat meta-llama/Llama-3.3-70B-Instruct-Turbo'); expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('meta-llama/Llama-3.3-70B-Instruct-Turbo'); @@ -70,7 +71,7 @@ describe('Together integration', () => { expect(embeddingsSpan).toBeDefined(); expect(embeddingsSpan!.name).toBe('embeddings togethercomputer/m2-bert-80M-8k-retrieval'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value).toBe('gen_ai.embeddings'); + expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); expect(embeddingsSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT]).toBeUndefined(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts index e4276d73a0dd..3da2f82f24c9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -14,6 +14,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -50,7 +51,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -65,7 +66,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -79,7 +80,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -93,7 +94,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -107,7 +108,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -120,7 +121,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_OP]: attr('gen_ai.execute_tool'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -144,7 +145,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -163,7 +164,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -181,7 +182,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -199,7 +200,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -216,7 +217,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -231,7 +232,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_TOOL_CALL_RESULT]: expect.objectContaining({ value: expect.any(String) }), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_OP]: attr('gen_ai.execute_tool'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -245,7 +246,7 @@ describe('Vercel AI integration (streaming v4)', () => { status: 'error', attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -258,7 +259,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), @@ -269,7 +270,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_OP]: attr('gen_ai.execute_tool'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), }), }), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts index 939ba08e9746..806649ede43f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -14,6 +14,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -47,7 +48,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -62,7 +63,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -76,7 +77,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -90,7 +91,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -104,7 +105,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -117,7 +118,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_OP]: attr('gen_ai.execute_tool'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -141,7 +142,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -160,7 +161,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -178,7 +179,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(20), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -196,7 +197,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -213,7 +214,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -228,7 +229,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_TOOL_CALL_RESULT]: expect.objectContaining({ value: expect.any(String) }), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_OP]: attr('gen_ai.execute_tool'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -241,7 +242,7 @@ describe('Vercel AI integration (streaming, v6)', () => { name: 'invoke_agent', attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.invoke_agent'), + [SENTRY_OP]: attr('gen_ai.invoke_agent'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -254,7 +255,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_OUTPUT_TOKENS]: attr(25), [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.generate_content'), + [SENTRY_OP]: attr('gen_ai.generate_content'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), @@ -265,7 +266,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: attr('call-1'), [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: attr('gen_ai.execute_tool'), + [SENTRY_OP]: attr('gen_ai.execute_tool'), [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), }), }), diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index b67a20a788f8..fcdbc89ee7f6 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -120,6 +120,7 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 5d0c37614a3e..77b40c7da02d 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -95,6 +95,7 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/browser-utils/src/web-vitals/emitSpan.ts b/packages/browser-utils/src/web-vitals/emitSpan.ts index ba56bf783f57..ed3602f35e6f 100644 --- a/packages/browser-utils/src/web-vitals/emitSpan.ts +++ b/packages/browser-utils/src/web-vitals/emitSpan.ts @@ -1,12 +1,5 @@ import type { Integration, Span, SpanAttributes } from '@sentry/core'; -import { - getClient, - getCurrentScope, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanToJSON, -} from '@sentry/core'; +import { getClient, getCurrentScope, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core/browser'; import { SENTRY_EXCLUSIVE_TIME, @@ -16,6 +9,7 @@ import { SENTRY_SEGMENT_NAME, SENTRY_TRANSACTION, USER_AGENT_ORIGINAL, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { WINDOW } from '../types'; import type { MetricNavigationType } from '../instrumentation/performanceObserver'; @@ -92,7 +86,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_OP]: op, [SENTRY_EXCLUSIVE_TIME]: 0, [`browser.web_vital.${metricName}.value`]: value, // oxlint-disable-next-line typescript-eslint/no-deprecated @@ -103,7 +97,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { ...passedAttributes, }; - if (parentSpan && spanToJSON(parentSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'pageload') { + if (parentSpan && spanToJSON(parentSpan).attributes[SENTRY_OP] === 'pageload') { // for LCP and CLS, we collect the pageload span id as an attribute attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId; } diff --git a/packages/browser-utils/src/web-vitals/softNavs.ts b/packages/browser-utils/src/web-vitals/softNavs.ts index addab70c50b0..9c7c03560df5 100644 --- a/packages/browser-utils/src/web-vitals/softNavs.ts +++ b/packages/browser-utils/src/web-vitals/softNavs.ts @@ -1,6 +1,6 @@ import type { Client, Span } from '@sentry/core'; -import { debug, LRUMap, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; -import { BROWSER_NAVIGATION_ID } from '@sentry/conventions/attributes'; +import { debug, LRUMap, spanToJSON } from '@sentry/core'; +import { BROWSER_NAVIGATION_ID, SENTRY_OP } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../debug-build'; import type { PerformanceSoftNavigation } from '../instrumentation/performanceObserver'; import { addPerformanceInstrumentationHandler, isPerformanceEventTiming } from '../instrumentation/performanceObserver'; @@ -113,7 +113,7 @@ export function startSoftNavigationCorrelation(client: Client): void { WINDOW.addEventListener('keydown', onInteraction, { capture: true, passive: true }); client.on('spanStart', span => { - if (spanToJSON(span).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'navigation') { + if (spanToJSON(span).attributes?.[SENTRY_OP] !== 'navigation') { return; } diff --git a/packages/browser-utils/src/web-vitals/spans.ts b/packages/browser-utils/src/web-vitals/spans.ts index 69bee44f19a3..0e60188b456f 100644 --- a/packages/browser-utils/src/web-vitals/spans.ts +++ b/packages/browser-utils/src/web-vitals/spans.ts @@ -7,7 +7,6 @@ import { getClient, getRootSpan, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, timestampInSeconds, UI_INTERACTION_CLICK_SPAN_NAME_FALLBACK, @@ -35,6 +34,7 @@ import { BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, BROWSER_WEB_VITAL_INP_TARGET, UI_COMPONENT_NAME, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { _emitWebVitalSpan } from './emitSpan'; import { isValidLcpMetric } from './lcp'; @@ -96,10 +96,7 @@ function trackWebVitalPerNavigation( // `back-forward-cache` navigation type, so matching on that alone lets the first of them replace the // navigation span, and every later vital then hangs off a sibling vital instead. const attributes = spanToJSON(span).attributes; - if ( - attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === NAVIGATION && - attributes[BROWSER_NAVIGATION_TYPE] === 'back-forward-cache' - ) { + if (attributes?.[SENTRY_OP] === NAVIGATION && attributes[BROWSER_NAVIGATION_TYPE] === 'back-forward-cache') { bfcacheNavigationSpan = span; } }); diff --git a/packages/browser-utils/test/performance/browserMetrics.test.ts b/packages/browser-utils/test/performance/browserMetrics.test.ts index d01d7b3c779a..bb87c4d64750 100644 --- a/packages/browser-utils/test/performance/browserMetrics.test.ts +++ b/packages/browser-utils/test/performance/browserMetrics.test.ts @@ -1,8 +1,8 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; import { getClient, getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentrySpan, setCurrentClient, @@ -246,7 +246,7 @@ describe('_addResourceSpans', () => { start_timestamp: timeOrigin + startTime, end_timestamp: timeOrigin + startTime + duration, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', + [SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.response.body.decoded_size']: entry.decodedBodySize, ['http.response.body.size']: entry.encodedBodySize, @@ -356,7 +356,7 @@ describe('_addResourceSpans', () => { _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465); expect(spans).toHaveLength(i + 1); - expect(spanToJSON(spans[i]!).attributes).toEqual(expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op })); + expect(spanToJSON(spans[i]!).attributes).toEqual(expect.objectContaining({ [SENTRY_OP]: op })); } }); @@ -429,7 +429,7 @@ describe('_addResourceSpans', () => { expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', + [SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.response.body.decoded_size']: entry.decodedBodySize, ['http.response.body.size']: entry.encodedBodySize, @@ -468,7 +468,7 @@ describe('_addResourceSpans', () => { expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', + [SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', 'url.domain': 'example.com', @@ -520,7 +520,7 @@ describe('_addResourceSpans', () => { expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', + [SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', 'url.domain': 'example.com', @@ -820,7 +820,7 @@ describe('_addNavigationSpans', () => { // `unloadEventStart`/`End` are 0 in the shared entry, so that span is never created. _addNavigationSpans(pageloadSpan, { ...entry, unloadEventStart: 1, unloadEventEnd: 2 }, 999, true); - const spanJson = spans.map(spanToJSON).find(span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op); + const spanJson = spans.map(spanToJSON).find(span => span.attributes[SENTRY_OP] === op); expect(spanJson?.name).toBe(expectedName); expect(spanJson?.attributes['url.full']).toBe('https://santry.com/test?q=secret#frag'); diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 6708e12549ba..cf066ee57cd2 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -78,6 +78,7 @@ export { } from '@sentry/core'; export { + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/browser/test/integrations/httpcontext.test.ts b/packages/browser/test/integrations/httpcontext.test.ts index 524c8742bf3a..555b68dd1c05 100644 --- a/packages/browser/test/integrations/httpcontext.test.ts +++ b/packages/browser/test/integrations/httpcontext.test.ts @@ -1,5 +1,6 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { describe, expect, it, vi } from 'vitest'; -import { httpContextIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../../src/exports'; +import { httpContextIntegration } from '../../src/exports'; import type { Event, StreamedSpanJSON } from '@sentry/core'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; import { BrowserClient } from '../../src/client'; @@ -29,7 +30,7 @@ describe('httpContextIntegration', () => { const span: Partial = { is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + [SENTRY_OP]: 'http.client', }, }; @@ -39,7 +40,7 @@ describe('httpContextIntegration', () => { expect(span.attributes).not.toHaveProperty('url.full'); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + [SENTRY_OP]: 'http.client', 'http.request.header.referer': ['https://example.com'], 'user_agent.original': USER_AGENT, 'sentry.is_localhost': false, @@ -52,7 +53,7 @@ describe('httpContextIntegration', () => { const span: Partial = { is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }, }; @@ -61,7 +62,7 @@ describe('httpContextIntegration', () => { integration.processSpan!(span as StreamedSpanJSON, browserClient); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', 'http.request.header.referer': ['https://example.com'], 'user_agent.original': USER_AGENT, 'sentry.is_localhost': false, @@ -74,14 +75,14 @@ describe('httpContextIntegration', () => { const span: Partial = { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.click', + [SENTRY_OP]: 'ui.click', }, }; integration.processSpan!(span as StreamedSpanJSON, new BrowserClient(getDefaultBrowserClientOptions())); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.click', + [SENTRY_OP]: 'ui.click', 'user_agent.original': USER_AGENT, 'sentry.is_localhost': false, }); @@ -161,13 +162,13 @@ describe('httpContextIntegration', () => { ); const span: Partial = { is_segment: true, - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload' }, + attributes: { [SENTRY_OP]: 'pageload' }, }; httpContextIntegration().processSpan!(span as StreamedSpanJSON, client); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', 'sentry.is_localhost': false, 'url.full': 'https://example.com', }); @@ -179,13 +180,13 @@ describe('httpContextIntegration', () => { ); const span: Partial = { is_segment: true, - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload' }, + attributes: { [SENTRY_OP]: 'pageload' }, }; httpContextIntegration().processSpan!(span as StreamedSpanJSON, client); expect(span.attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', 'url.full': 'https://example.com', 'http.request.header.referer': ['[Filtered]'], 'user_agent.original': USER_AGENT, diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index b61518c581f5..38bc3e745045 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -9,7 +9,6 @@ import { getDynamicSamplingContextFromSpan, getMainCarrier, metrics, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, @@ -35,7 +34,7 @@ import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/linkedTrace import { bfcacheMetricsIntegration } from '../../src/integrations/bfcacheMetrics'; import * as webVitalsModule from '../../src/integrations/webVitals'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; const oldTextEncoder = global.window.TextEncoder; const oldTextDecoder = global.window.TextDecoder; @@ -176,7 +175,7 @@ describe('browserTracingIntegration', () => { name: 'Pageload', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -320,7 +319,7 @@ describe('browserTracingIntegration', () => { name: 'Pageload', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -355,7 +354,7 @@ describe('browserTracingIntegration', () => { name: 'Navigation', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -398,7 +397,7 @@ describe('browserTracingIntegration', () => { name: 'Navigation', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -443,7 +442,7 @@ describe('browserTracingIntegration', () => { name: 'Pageload', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -478,7 +477,7 @@ describe('browserTracingIntegration', () => { expect(getSpanDescendants(span).map(span => spanToJSON(span))).toContainEqual( expect.objectContaining({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation.redirect', + [SENTRY_OP]: 'navigation.redirect', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', [URL_FULL]: 'https://example.com/test', [URL_PATH]: '/test', @@ -539,7 +538,7 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', @@ -615,7 +614,7 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', @@ -917,7 +916,7 @@ describe('browserTracingIntegration', () => { expect(span).toBeDefined(); expect(spanToJSON(span).attributes).toEqual( expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache', 'browser.navigation.type': 'back-forward-cache', }), @@ -1045,7 +1044,7 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', @@ -1102,7 +1101,7 @@ describe('browserTracingIntegration', () => { name: 'test span', status: 'ok', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d5b1d11ee4e5..e02142b940d6 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -116,6 +116,7 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/bun/test/integrations/bunHttpServer.test.ts b/packages/bun/test/integrations/bunHttpServer.test.ts index 48d5f255d821..62b008bafe4a 100644 --- a/packages/bun/test/integrations/bunHttpServer.test.ts +++ b/packages/bun/test/integrations/bunHttpServer.test.ts @@ -1,6 +1,6 @@ import http from 'node:http'; -import { HTTP_REQUEST_METHOD } from '@sentry/conventions/attributes'; -import { getActiveSpan, getCurrentScope, getTraceData, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; +import { HTTP_REQUEST_METHOD, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, getCurrentScope, getTraceData, spanToJSON } from '@sentry/core'; import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; import { init } from '../../src'; @@ -51,7 +51,7 @@ describe('Bun HTTP Server Integration', () => { await close(); expect(span).toBeDefined(); - expect(span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span?.attributes[SENTRY_OP]).toBe('http.server'); // No router resolves a route here, so with span streaming the name is the request method. expect(span?.name).toBe('GET'); expect(span?.attributes['url.path']).toBe('/users'); @@ -82,7 +82,7 @@ describe('Bun HTTP Server Integration', () => { await close(); expect(span).toBeDefined(); - expect(span?.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span?.attributes[SENTRY_OP]).toBe('http.server'); expect(span?.name).toBe('QUERY'); expect(span?.attributes['url.path']).toBe('/search'); expect(span?.attributes[HTTP_REQUEST_METHOD]).toBe('QUERY'); diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 59f8713b181e..0b583ec88d9b 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -86,6 +86,7 @@ export { instrumentSupabaseClient, zodErrorsIntegration, consoleIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index f77eb63b8f56..5f3ed230f85e 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -16,6 +16,7 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_PREVIOUS_TRACE_SAMPLE_RATE = 'sentry.prev /** * Use this attribute to represent the operation of a span. + * @deprecated Use `SENTRY_OP` from `@sentry/conventions/attributes` instead. */ export const SEMANTIC_ATTRIBUTE_SENTRY_OP = 'sentry.op'; diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index abd27b52edf1..7d149353da1a 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,12 +1,13 @@ /* eslint-disable max-lines */ import { getClient, getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; -import { SENTRY_PROFILE_ID, SENTRY_EXCLUSIVE_TIME, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; import { - SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '../semanticAttributes'; + SENTRY_PROFILE_ID, + SENTRY_EXCLUSIVE_TIME, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, +} from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { Client } from '../client'; import type { TransactionEvent } from '../types/event'; import type { SpanLink } from '../types/link'; @@ -103,7 +104,7 @@ export class SentrySpan implements Span { this._attributes = {}; this.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: spanContext.op, + [SENTRY_OP]: spanContext.op, ...spanContext.attributes, }); @@ -270,7 +271,7 @@ export class SentrySpan implements Span { return { data: this._attributes, description: this._name, - op: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], + op: this._attributes[SENTRY_OP], parent_span_id: this._parentSpanId, span_id: this._spanId, start_timestamp: this._startTime, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 1aa3818a9f1c..ff312712ee46 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -6,8 +6,8 @@ import { getMainCarrier } from '../carrier'; import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import type { Scope } from '../scope'; -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; import type { ClientOptions } from '../types/options'; import type { SentrySpanArguments, Span, SpanTimeInput } from '../types/span'; import type { StartSpanOptions } from '../types/startSpanOptions'; @@ -448,7 +448,7 @@ function parseSentrySpanArguments(options: StartSpanOptions): SentrySpanArgument // constructor only adds it after the sampling decision. An explicit `sentry.op` attribute wins. if (options.op) { initialCtx.attributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: options.op, + [SENTRY_OP]: options.op, ...options.attributes, }; } @@ -633,7 +633,7 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se return shouldIgnoreSpan( { description: spanArguments.name || '', - op: spanArguments.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] || spanArguments.op, + op: spanArguments.attributes?.[SENTRY_OP] || spanArguments.op, attributes: spanArguments.attributes, }, ignoreSpans, diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 2bf38cdb5864..a643caeccaba 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -5,12 +5,8 @@ import { serializeAttributes } from '../attributes'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; import type { Scope } from '../scope'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_STATUS_MESSAGE } from '@sentry/conventions/attributes'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '../semanticAttributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_STATUS_MESSAGE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { isStatusErrorMessageValid, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; import { getCapturedScopesOnSpan } from '../tracing/utils'; @@ -190,7 +186,7 @@ export function spanToStaticSpanJSON(span: Span): SpanJSON { // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time timestamp: spanTimeInputToSeconds(endTime) || undefined, status: getStatusMessage(status), - op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], + op: attributes[SENTRY_OP], origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, links: convertSpanLinksForEnvelope(links), }; diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 06f83bea6b0a..5f72e42a622b 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -3,7 +3,6 @@ import type { Contexts, Span, StreamedSpanJSON } from '../../../../src'; import { captureSpan, debug, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanStreamingIntegration, @@ -33,6 +32,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, + SENTRY_OP, } from '@sentry/conventions/attributes'; describe('captureSpan', () => { @@ -77,7 +77,7 @@ describe('captureSpan', () => { type: 'string', value: 'stream', }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'http.client', }, @@ -173,7 +173,7 @@ describe('captureSpan', () => { status: 'ok', is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'http.client', }, @@ -269,7 +269,7 @@ describe('captureSpan', () => { status: 'ok', is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + [SENTRY_OP]: { type: 'string', value: 'http.client', }, @@ -350,7 +350,7 @@ describe('captureSpan', () => { is_segment: true, attributes: { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SEGMENT_NAME]: { value: 'my-span', type: 'string' }, diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index e36effb86317..9f54b7e693c9 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -6,7 +6,6 @@ import { getMainCarrier, getTraceData, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setAsyncContextStrategy, setCurrentClient, @@ -33,7 +32,7 @@ import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '. import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; import { SUPPRESS_TRACING_KEY } from '../../../src/tracing/constants'; import { resetGlobals } from '../../testutils'; -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; const enum Type { Sync = 'sync', @@ -110,14 +109,14 @@ describe('startSpan', () => { }); try { await startSpan({ name: 'GET users/[id]' }, span => { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); + span.setAttribute(SENTRY_OP, 'http.server'); return callback(); }); } catch { // } - expect(spanToJSON(_span!).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual('http.server'); + expect(spanToJSON(_span!).attributes[SENTRY_OP]).toEqual('http.server'); }); it('creates a span with correct description', async () => { @@ -156,7 +155,7 @@ describe('startSpan', () => { try { await startSpan({ name: 'GET users/[id]' }, () => { return startSpan({ name: 'SELECT * from users' }, childSpan => { - childSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); + childSpan.setAttribute(SENTRY_OP, 'db.query'); return callback(); }); }); @@ -168,7 +167,7 @@ describe('startSpan', () => { const spans = getSpanDescendants(_span!); expect(spans).toHaveLength(2); - expect(spanToJSON(spans[1]!).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual('db.query'); + expect(spanToJSON(spans[1]!).attributes[SENTRY_OP]).toEqual('db.query'); }); it('correctly sets the span origin', async () => { diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index e8ae4379b86f..c11a2fa3317d 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,10 +1,9 @@ -import { SENTRY_LINK_TYPE, SENTRY_STATUS_MESSAGE } from '@sentry/conventions/attributes'; +import { SENTRY_LINK_TYPE, SENTRY_STATUS_MESSAGE, SENTRY_OP } from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, test } from 'vitest'; import { convertSpanLinksForEnvelope, getCurrentScope, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, SentrySpan, @@ -400,7 +399,7 @@ describe('spanToStaticSpanJSON', () => { attributes: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', }, status: { code: SPAN_STATUS_ERROR, message: 'unknown_error' }, @@ -417,7 +416,7 @@ describe('spanToStaticSpanJSON', () => { data: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', }, status: 'unknown_error', @@ -488,7 +487,7 @@ describe('spanToStaticSpanJSON', () => { attr2: 2, attr3: true, attr4: [1, 2, 3], - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', }, links: [ @@ -586,7 +585,7 @@ describe('spanToStaticSpanJSON', () => { attributes: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', }, links: [ @@ -616,7 +615,7 @@ describe('spanToStaticSpanJSON', () => { attributes: { attr1: 'value1', attr2: 2, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', [SENTRY_STATUS_MESSAGE]: 'unknown_error', }, @@ -683,7 +682,7 @@ describe('spanToStaticSpanJSON', () => { attr2: 2, attr3: true, attr4: [1, 2, 3], - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SENTRY_OP]: 'test op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', }, links: [ @@ -712,7 +711,7 @@ describe('spanToStaticSpanJSON', () => { attr2: { type: 'integer', value: 2 }, attr3: { type: 'boolean', value: true }, attr4: { type: 'array', value: [1, 2, 3] }, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test op' }, + [SENTRY_OP]: { type: 'string', value: 'test op' }, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, }, links: [ diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 8db992d91e6a..cccdbf5f479c 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -81,6 +81,7 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts index a7aace022282..66ccf6cb9e0a 100644 --- a/packages/effect/test/tracer.test.ts +++ b/packages/effect/test/tracer.test.ts @@ -1,7 +1,8 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; import * as sentryCoreBrowser from '@sentry/core/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { ServerRuntimeClient } from '@sentry/core/server'; import { Effect } from 'effect'; import * as Tracer from 'effect/Tracer'; @@ -202,9 +203,9 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa const attributes = yield* attributesFor('my-operation'); expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(attributes?.[SENTRY_OP]).toBeUndefined(); expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN); - expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_OP); + expect(attributes).not.toHaveProperty(SENTRY_OP); }), ); @@ -213,7 +214,7 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa const attributes = yield* attributesFor('http.server GET /api/users'); expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(attributes?.[SENTRY_OP]).toBe('http.server'); }), ); @@ -222,7 +223,7 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa const attributes = yield* attributesFor('http.client GET https://api.example.com'); expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.client'); + expect(attributes?.[SENTRY_OP]).toBe('http.client'); }), ); diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index cf35f6de4eb8..e5ddb8ee3726 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -95,6 +95,7 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/elysia/src/withElysia.ts b/packages/elysia/src/withElysia.ts index c68c192e3fa4..f423920872cc 100644 --- a/packages/elysia/src/withElysia.ts +++ b/packages/elysia/src/withElysia.ts @@ -18,7 +18,6 @@ import { getTraceData, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, startInactiveSpan, @@ -154,7 +153,7 @@ function instrumentLifecyclePhase( name: isStreamedRequestHandlerSpan ? context.route || REQUEST_HANDLER_SPAN_NAME_FALLBACK : phaseName, parentSpan: rootSpan, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_OP]: op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, ...routeAttribute, }, @@ -169,7 +168,7 @@ function instrumentLifecyclePhase( name: isStreamedRequestHandlerSpan ? context.route || REQUEST_HANDLER_SPAN_NAME_FALLBACK : handlerName, parentSpan: phaseSpan, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_OP]: op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, ...routeAttribute, // Streamed request handler spans are named after the route, so the diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index d789d83c91a8..8404d252e4b3 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -96,6 +96,7 @@ export { getAutoPerformanceIntegrations, cron, parameterize, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index e4690c988d7a..a7e41bacc1b2 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -1,6 +1,6 @@ import type { MonitorConfig } from '@sentry/core'; -import { CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as Sentry from '@sentry/node'; import { startSpan } from '@sentry/node'; import { isExpectedError } from './helpers'; @@ -43,7 +43,7 @@ export function SentryTraced(op: string = 'function') { name: propertyKey, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: op, + [SENTRY_OP]: op, [CODE_FUNCTION_NAME]: propertyKey, }, }, diff --git a/packages/nestjs/test/decorators.test.ts b/packages/nestjs/test/decorators.test.ts index 9640244de437..8e6c083057ff 100644 --- a/packages/nestjs/test/decorators.test.ts +++ b/packages/nestjs/test/decorators.test.ts @@ -1,7 +1,7 @@ import 'reflect-metadata'; -import { CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; import * as core from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SentryCron, SentryExceptionCaptured, SentryTraced } from '../src/decorators'; import * as helpers from '../src/helpers'; @@ -42,7 +42,7 @@ describe('SentryTraced decorator', () => { name: 'testMethod', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test-operation', + [SENTRY_OP]: 'test-operation', [CODE_FUNCTION_NAME]: 'testMethod', }, }, @@ -76,7 +76,7 @@ describe('SentryTraced decorator', () => { name: 'testDefaultOp', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [CODE_FUNCTION_NAME]: 'testDefaultOp', }, }, @@ -110,7 +110,7 @@ describe('SentryTraced decorator', () => { name: 'syncMethod', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'sync-operation', + [SENTRY_OP]: 'sync-operation', [CODE_FUNCTION_NAME]: 'syncMethod', }, }, diff --git a/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts b/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts index e80079fe68bd..be2c8b998316 100644 --- a/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts +++ b/packages/nextjs/src/common/utils/liveRootSpanAdapter.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import type { Span } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToStaticSpanJSON } from '@sentry/core'; +import { spanToStaticSpanJSON } from '@sentry/core'; import type { MutableRootSpan } from '../../server/enhanceHandleRequestRootSpan'; /** @@ -24,7 +24,7 @@ export function createLiveRootSpanAdapter(span: Span): MutableRootSpan { span.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, source); }, setOp: (op: string) => { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, op); + span.setAttribute(SENTRY_OP, op); }, }; } diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index c971bbafec91..564b8c4f39e6 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -10,7 +10,6 @@ import { hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, registerSpanErrorInstrumentation, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; @@ -165,7 +164,7 @@ export function init(options: VercelEdgeOptions = {}): void { // Make sure middleware spans get the right op if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, MIDDLEWARE); + span.setAttribute(SENTRY_OP, MIDDLEWARE); span.setAttribute(SENTRY_SEGMENT_NAME_SOURCE, 'url'); } diff --git a/packages/nextjs/test/common/liveRootSpanAdapter.test.ts b/packages/nextjs/test/common/liveRootSpanAdapter.test.ts index cd2f7a9fcac3..3e935f4fe8f9 100644 --- a/packages/nextjs/test/common/liveRootSpanAdapter.test.ts +++ b/packages/nextjs/test/common/liveRootSpanAdapter.test.ts @@ -1,5 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SentrySpan, spanToStaticSpanJSON } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SentrySpan, spanToStaticSpanJSON } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { createLiveRootSpanAdapter } from '../../src/common/utils/liveRootSpanAdapter'; @@ -12,7 +12,7 @@ describe('createLiveRootSpanAdapter', () => { expect(adapter.attributes.foo).toBe('bar'); adapter.setOp('http.server'); - expect(spanToStaticSpanJSON(span).data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(spanToStaticSpanJSON(span).data[SENTRY_OP]).toBe('http.server'); }); it('renames the span without stamping source=custom (preserves an existing source)', () => { diff --git a/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts b/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts index c25f9d79e650..743bb32b8d02 100644 --- a/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts +++ b/packages/nextjs/test/server/enhanceHandleRequestRootSpan.test.ts @@ -3,8 +3,8 @@ import { HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, HTTP_STATUS_CODE, + SENTRY_OP, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../../src/common/nextSpanAttributes'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../../src/common/span-attributes-with-logic-attached'; @@ -35,7 +35,7 @@ describe('enhanceHandleRequestRootSpan', () => { enhanceHandleRequestRootSpan(span); expect(getName()).toBe('GET /api/foo'); expect(getOp()).toBeUndefined(); - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(span.attributes[SENTRY_OP]).toBeUndefined(); }); it('sets http.server op and source=route for parameterized routes', () => { @@ -50,7 +50,7 @@ describe('enhanceHandleRequestRootSpan', () => { enhanceHandleRequestRootSpan(span); expect(getOp()).toBe('http.server'); - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(span.attributes[SENTRY_OP]).toBe('http.server'); expect(getName()).toBe('GET /api/users/[id]'); expect(span.attributes[SENTRY_SEGMENT_NAME_SOURCE]).toBe('route'); expect(span.attributes[ATTR_NEXT_ROUTE]).toBe('/api/users/[id]'); @@ -192,14 +192,14 @@ describe('enhanceHandleRequestRootSpan', () => { name = n; }, setOp: (op: string) => { - attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op; + attributes[SENTRY_OP] = op; }, }; enhanceHandleRequestRootSpan(span); expect(name).toBe('middleware GET'); - expect(attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('middleware'); + expect(attributes[SENTRY_OP]).toBe('middleware'); }); it('rewrites GET /_error using the http.target attribute', () => { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index deed6c7c0b65..4ca920d43784 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -113,6 +113,7 @@ export { setAttributes, setUser, setConversationId, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts index c2effd4a23f1..23660cc7f7ff 100644 --- a/packages/node/src/integrations/fs/vendored/instrumentation.ts +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -14,11 +14,10 @@ * `error.type` (the syscall error code, e.g. `ENOENT`). */ -import { ERROR_TYPE } from '@sentry/conventions/attributes'; +import { ERROR_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; import { FILE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, getActiveSpan, @@ -101,7 +100,7 @@ function getSpanAttributes( config: FsInstrumentationConfig, ): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: SPAN_OP, + [SENTRY_OP]: SPAN_OP, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN, }; diff --git a/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts b/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts index fec51364d935..9d6a84fa318f 100644 --- a/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts +++ b/packages/nuxt/test/runtime/hooks/updateRouteBeforeResponse.test.ts @@ -1,5 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, SentrySpan } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, SentrySpan } from '@sentry/core'; import { afterEach, describe, expect, it, type Mock, vi } from 'vitest'; import { updateRouteBeforeResponse } from '../../../src/runtime/hooks/updateRouteBeforeResponse'; @@ -15,7 +15,7 @@ vi.mock(import('@sentry/core'), async importOriginal => { describe('updateRouteBeforeResponse', () => { const mockRootSpan = new SentrySpan({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', }, }); mockRootSpan.updateName = vi.fn(); diff --git a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts index d7fb10fe2eef..c91f26abf59b 100644 --- a/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts +++ b/packages/nuxt/test/runtime/hooks/wrapMiddlewareHandler.test.ts @@ -1,3 +1,4 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import * as SentryCore from '@sentry/core'; import * as SentryCoreServer from '@sentry/core/server'; import type { EventHandler, EventHandlerRequest, H3Event } from 'h3'; @@ -187,7 +188,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'object-middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'object-middleware', }), }), @@ -218,7 +219,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'request-middleware.onRequest', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'request-middleware', 'nuxt.middleware.hook.name': 'onRequest', }), @@ -305,7 +306,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'response-middleware.onBeforeResponse', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'response-middleware', 'nuxt.middleware.hook.name': 'onBeforeResponse', }), @@ -517,7 +518,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'v2-middleware.middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'v2-middleware', 'nuxt.middleware.hook.name': 'middleware', 'nuxt.middleware.hook.index': 0, @@ -596,7 +597,7 @@ describe('wrapMiddlewareHandlerWithSentry', () => { expect.objectContaining({ name: 'api-middleware', attributes: expect.objectContaining({ - [SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware', + [SENTRY_OP]: 'middleware', 'nuxt.middleware.name': 'api-middleware', 'http.request.method': 'GET', 'http.route': '/test-path', diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 1b967dfc051a..957f034b801d 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,7 +1,7 @@ /* eslint-disable typescript/no-deprecated */ import type { Span, TimeInput } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_KIND } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_KIND, SENTRY_OP } from '@sentry/conventions/attributes'; import type { Event, Scope } from '@sentry/core'; import { getCapturedScopesOnSpan, @@ -10,7 +10,6 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToJSON, @@ -232,7 +231,7 @@ describe('trace', () => { expect(getSpanAttributes(span)).toEqual({ [SENTRY_SEGMENT_NAME_SOURCE]: 'task', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'my-op', + [SENTRY_OP]: 'my-op', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }); }, @@ -627,7 +626,7 @@ describe('trace', () => { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'task', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'my-op', + [SENTRY_OP]: 'my-op', }); }); diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 2110d4835cf0..2dbc01907fe6 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -12,7 +12,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; @@ -287,7 +286,7 @@ function getActiveRootSpan(): Span | undefined { return undefined; } - const op = spanToJSON(rootSpan).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const op = spanToJSON(rootSpan).attributes[SENTRY_OP]; // Only use this root span if it is a pageload or navigation span return op === 'navigation' || op === 'pageload' ? rootSpan : undefined; diff --git a/packages/react/test/react-router.test.tsx b/packages/react/test/react-router.test.tsx index cb100633002f..7422c57878da 100644 --- a/packages/react/test/react-router.test.tsx +++ b/packages/react/test/react-router.test.tsx @@ -5,14 +5,8 @@ * directly from `react` / `react-router` so `reactRouterBrowserTracingIntegration()` can be used * without passing them in. */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; import { MemoryRouter, Route, Routes, useNavigate } from 'react-router'; @@ -78,7 +72,7 @@ describe('@sentry/react/react-router', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', // version-agnostic origin (no `_v6`/`_v7` suffix) [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter', }, @@ -120,7 +114,7 @@ describe('@sentry/react/react-router', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter', }, }); diff --git a/packages/react/test/reactrouter-cross-usage.test.tsx b/packages/react/test/reactrouter-cross-usage.test.tsx index 3711626a27c2..6b312e8987ba 100644 --- a/packages/react/test/reactrouter-cross-usage.test.tsx +++ b/packages/react/test/reactrouter-cross-usage.test.tsx @@ -1,14 +1,8 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { render, waitFor } from '@testing-library/react'; import * as React from 'react'; import { act } from 'react'; @@ -667,7 +661,7 @@ describe('React Router cross usage of wrappers', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/settings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -687,8 +681,8 @@ describe('React Router cross usage of wrappers', () => { const calls = mockStartBrowserTracingNavigationSpan.mock.calls; expect(calls[0]![1].name).toBe('/settings'); expect(calls[1]![1].name).toBe('/profile'); - expect(calls[0]![1].attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('navigation'); - expect(calls[1]![1].attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('navigation'); + expect(calls[0]![1].attributes[SENTRY_OP]).toBe('navigation'); + expect(calls[1]![1].attributes[SENTRY_OP]).toBe('navigation'); }); it('should create separate transactions for rapid consecutive navigations', async () => { @@ -786,7 +780,7 @@ describe('React Router cross usage of wrappers', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/user/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -808,7 +802,7 @@ describe('React Router cross usage of wrappers', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/user/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -864,7 +858,7 @@ describe('React Router cross usage of wrappers', () => { expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { name: '/settings/*', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }), }); }); @@ -912,7 +906,7 @@ describe('React Router cross usage of wrappers', () => { expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledWith(expect.any(BrowserClient), { name: '/details/*', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }), }); }); diff --git a/packages/react/test/reactrouter-descendant-routes.test.tsx b/packages/react/test/reactrouter-descendant-routes.test.tsx index 10804904f5e5..4754f55f4e55 100644 --- a/packages/react/test/reactrouter-descendant-routes.test.tsx +++ b/packages/react/test/reactrouter-descendant-routes.test.tsx @@ -1,14 +1,8 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -177,7 +171,7 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/child/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -309,7 +303,7 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/:orgId/:id', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -420,7 +414,7 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -481,7 +475,7 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -634,7 +628,7 @@ describe('React Router Descendant Routes', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index e77d9584499b..24546d9eeffa 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -2,14 +2,8 @@ * @vitest-environment jsdom */ import { BrowserClient } from '@sentry/browser'; -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { render } from '@testing-library/react'; import * as React from 'react'; import { act } from 'react'; @@ -109,7 +103,7 @@ describe('browserTracingReactRouterV3', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }, }); }); @@ -146,7 +140,7 @@ describe('browserTracingReactRouterV3', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); @@ -160,7 +154,7 @@ describe('browserTracingReactRouterV3', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/features', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); }); @@ -202,7 +196,7 @@ describe('browserTracingReactRouterV3', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/:userid', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(getCurrentScope().getScopeData().transactionName).toEqual('/users/:userid'); @@ -218,7 +212,7 @@ describe('browserTracingReactRouterV3', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/teams/:teamId/details', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(getCurrentScope().getScopeData().transactionName).toEqual('/teams/:teamId/details'); diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 98c4da18df5a..c1d6bff53432 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,20 +1,14 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-4'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { BrowserClient, reactRouterV4BrowserTracingIntegration, withSentryRouting } from '../src'; import type { RouteConfig } from '../src/reactrouter'; @@ -87,7 +81,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }, }); }); @@ -132,7 +126,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); @@ -145,7 +139,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); }); @@ -204,7 +198,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); }); @@ -241,7 +235,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -286,7 +280,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -309,7 +303,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(3); @@ -358,7 +352,7 @@ describe('browserTracingReactRouterV4', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/organizations/:orgid/v1/:teamid', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); @@ -372,7 +366,7 @@ describe('browserTracingReactRouterV4', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/organizations/:orgid', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); }); diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 489735c46d47..7e25b9fbb4bc 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,20 +1,14 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-5'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { BrowserClient, reactRouterV5BrowserTracingIntegration, withSentryRouting } from '../src'; import type { RouteConfig } from '../src/reactrouter'; @@ -87,7 +81,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', }, }); }); @@ -132,7 +126,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); @@ -145,7 +139,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); }); @@ -204,7 +198,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); }); @@ -241,7 +235,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -286,7 +280,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(2); @@ -309,7 +303,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', }, }); expect(mockRootSpan.updateName).toHaveBeenCalledTimes(3); @@ -357,7 +351,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [URL_TEMPLATE]: '/organizations/:orgid/v1/:teamid', }, }); @@ -371,7 +365,7 @@ describe('browserTracingReactRouterV5', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [URL_TEMPLATE]: '/organizations/:orgid', }, }); diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index b4b94eb6c5ca..d22f2e445d49 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,14 +1,8 @@ /** * @vitest-environment jsdom */ -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; import type { RouteObject } from 'react-router-6'; @@ -214,7 +208,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -261,7 +255,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -298,7 +292,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -416,7 +410,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -503,7 +497,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/us', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -541,7 +535,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/:page', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -581,7 +575,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/stores/:storeId/products/:productId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -629,7 +623,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -679,7 +673,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -731,7 +725,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -771,7 +765,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -811,7 +805,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -883,7 +877,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -1035,7 +1029,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1087,7 +1081,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/us', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1139,7 +1133,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/:page', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1197,7 +1191,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/stores/:storeId/products/:productId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1279,7 +1273,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1359,7 +1353,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/param-page/:id/details/:superId', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1516,7 +1510,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -1556,7 +1550,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 543dd239a67e..36629df62a35 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -106,6 +106,7 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index f18b713dac6b..00bc981926d9 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -84,6 +84,7 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/server-utils/src/ai/mastra/index.ts b/packages/server-utils/src/ai/mastra/index.ts index 10ca720d976c..48db51f77fcc 100644 --- a/packages/server-utils/src/ai/mastra/index.ts +++ b/packages/server-utils/src/ai/mastra/index.ts @@ -5,12 +5,11 @@ import { getActiveSpan, getClient, LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; -import { GEN_AI_RESPONSE_MODEL } from '@sentry/conventions/attributes'; +import { GEN_AI_RESPONSE_MODEL, SENTRY_OP } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../../debug-build'; import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; @@ -144,7 +143,7 @@ export class SentryMastraExporter implements MastraObservabilityExporter { parentSpan: parentSpan ?? activeSpan, attributes: { ...this._attributesFor(span), - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: getOperation(span.type)?.op, + [SENTRY_OP]: getOperation(span.type)?.op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MASTRA_ORIGIN, }, }); diff --git a/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts index 68f7c832fe14..57aea8904715 100644 --- a/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts @@ -1,10 +1,14 @@ import type { TracingChannel } from 'node:diagnostics_channel'; -import { GRAPHQL_DOCUMENT, GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE } from '@sentry/conventions/attributes'; +import { + GRAPHQL_DOCUMENT, + GRAPHQL_OPERATION_NAME, + GRAPHQL_OPERATION_TYPE, + SENTRY_OP, +} from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import { getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, @@ -160,7 +164,7 @@ function setupParseChannel(tracingChannel: GraphqlTracingChannelFactory): void { name: client && hasSpanStreamingEnabled(client) ? `GraphQL ${PROCESSING_TYPE_PARSE}` : SPAN_NAME_PARSE, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_PARSE, }, }); @@ -177,7 +181,7 @@ function setupValidateChannel(tracingChannel: GraphqlTracingChannelFactory): voi name: client && hasSpanStreamingEnabled(client) ? `GraphQL ${PROCESSING_TYPE_VALIDATE}` : SPAN_NAME_VALIDATE, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_VALIDATE, [GRAPHQL_DOCUMENT]: collectGraphqlDocument(data.document), }, @@ -213,7 +217,7 @@ function setupOperationChannel( : getOperationSpanName(data.operationType, data.operationName, fallbackName), attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_EXECUTE, [GRAPHQL_OPERATION_TYPE]: data.operationType, [GRAPHQL_OPERATION_NAME]: data.operationName || undefined, @@ -255,7 +259,7 @@ function setupResolveChannel(tracingChannel: GraphqlTracingChannelFactory, ignor : `${SPAN_NAME_RESOLVE} ${data.fieldPath}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_RESOLVE, [GRAPHQL_FIELD_NAME]: data.fieldName, [GRAPHQL_FIELD_PATH]: data.fieldPath, diff --git a/packages/server-utils/src/integrations/graphql/resolvers.ts b/packages/server-utils/src/integrations/graphql/resolvers.ts index 8e028d939ee8..d5c36ccd4915 100644 --- a/packages/server-utils/src/integrations/graphql/resolvers.ts +++ b/packages/server-utils/src/integrations/graphql/resolvers.ts @@ -6,13 +6,13 @@ * call runs). Resolver spans use the same origin/op/field attributes as the native subscriber. */ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, @@ -183,7 +183,7 @@ function createFieldIfNotExists( function createResolverSpan(info: GraphQLResolveInfo, path: string[], parentSpan?: Span): Span { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_RESOLVE, [GRAPHQL_FIELD_NAME]: info.fieldName, [GRAPHQL_FIELD_PATH]: path.join('.'), diff --git a/packages/server-utils/src/integrations/graphql/spans.ts b/packages/server-utils/src/integrations/graphql/spans.ts index 16f27d8a5a9c..fb0ef6a624b7 100644 --- a/packages/server-utils/src/integrations/graphql/spans.ts +++ b/packages/server-utils/src/integrations/graphql/spans.ts @@ -5,13 +5,17 @@ * arguments of the injected `parse`/`validate`/`execute` calls rather than graphql's native events. */ -import { GRAPHQL_DOCUMENT, GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE } from '@sentry/conventions/attributes'; +import { + GRAPHQL_DOCUMENT, + GRAPHQL_OPERATION_NAME, + GRAPHQL_OPERATION_TYPE, + SENTRY_OP, +} from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import { getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, @@ -41,7 +45,7 @@ import type { const BASE_ATTRIBUTES = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GRAPHQL, + [SENTRY_OP]: GRAPHQL, } as const; export function startParseSpan(): Span { diff --git a/packages/server-utils/src/integrations/redis/redis-cache.ts b/packages/server-utils/src/integrations/redis/redis-cache.ts index ac74bee358f4..bdc860872a6d 100644 --- a/packages/server-utils/src/integrations/redis/redis-cache.ts +++ b/packages/server-utils/src/integrations/redis/redis-cache.ts @@ -7,17 +7,11 @@ import { NETWORK_PEER_PORT, SERVER_ADDRESS, SERVER_PORT, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - CACHE_OPERATION_NAMES, - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - spanToJSON, - truncate, -} from '@sentry/core'; +import { CACHE_OPERATION_NAMES, getClient, hasSpanStreamingEnabled, spanToJSON, truncate } from '@sentry/core'; // Runtime-agnostic on purpose: imports nothing from `node:*` and touches `Buffer` only inside // function bodies (never at module load), so bundling this into an edge runtime is safe and the @@ -174,7 +168,7 @@ export function getRedisCacheAttributes( } const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation, + [SENTRY_OP]: cacheOperation, [CACHE_KEY]: safeKey, [CACHE_OPERATION]: CACHE_OPERATION_NAMES[cacheOperation], }; @@ -208,7 +202,7 @@ export function getRedisCacheAttributes( * meaningless. */ export function applyCacheResponseAttributes(span: Span, response: unknown): void { - const op = spanToJSON(span).attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const op = spanToJSON(span).attributes[SENTRY_OP]; if (op !== CACHE_GET && op !== CACHE_PUT) { return; } diff --git a/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts b/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts index bc282e0348bc..8e7c4d840d11 100644 --- a/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts @@ -11,11 +11,11 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_OP, } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, @@ -70,7 +70,7 @@ describe('instrumentWorkersAiClient', () => { */ const ALWAYS_RECORDED = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', @@ -188,7 +188,7 @@ describe('instrumentWorkersAiClient', () => { expect(span.description).toBe('evaluate typesafe/jev'); expect(span.data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.evaluate', + [SENTRY_OP]: 'gen_ai.evaluate', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [GEN_AI_PROVIDER_NAME]: 'cloudflare.workers_ai', diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index a9e133da3a97..41f440b66fe0 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -21,7 +21,6 @@ import { getClient, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, } from '@sentry/core'; @@ -153,7 +152,7 @@ function withSentryRouterRoot(Root: Component): Component { name: 'Pageload', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), @@ -105,7 +99,7 @@ describe('solidRouterBrowserTracingIntegration', () => { name: 'Pageload', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), @@ -150,7 +144,7 @@ describe('solidRouterBrowserTracingIntegration', () => { expect(span.name).toBe(parametrizedRoute); expect(span.attributes).toMatchObject({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', }); @@ -186,7 +180,7 @@ describe('solidRouterBrowserTracingIntegration', () => { name: '/about', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', }), }), diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index bc01f6bb9906..d8a151159551 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -88,6 +88,7 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/solidstart/test/client/solidrouter.test.tsx b/packages/solidstart/test/client/solidrouter.test.tsx index 226868db256d..8a64b1f1c5ac 100644 --- a/packages/solidstart/test/client/solidrouter.test.tsx +++ b/packages/solidstart/test/client/solidrouter.test.tsx @@ -1,13 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import { spanToJSON } from '@sentry/browser'; import type { Span } from '@sentry/core'; -import { - createTransport, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, -} from '@sentry/core'; +import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; import type { MemoryHistory } from '@solidjs/router'; import { createMemoryHistory, MemoryRouter, Navigate, Route } from '@solidjs/router'; import { render, waitFor } from '@solidjs/testing-library'; @@ -74,7 +68,7 @@ describe('solidRouterBrowserTracingIntegration', () => { name: 'Pageload', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), @@ -105,7 +99,7 @@ describe('solidRouterBrowserTracingIntegration', () => { name: '/', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + [SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), @@ -150,7 +144,7 @@ describe('solidRouterBrowserTracingIntegration', () => { expect(span.name).toBe(parametrizedRoute); expect(span.attributes).toMatchObject({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', }); @@ -186,7 +180,7 @@ describe('solidRouterBrowserTracingIntegration', () => { name: '/about', attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', }), }), diff --git a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts index b196f7a78a2a..2fe8b328b9ad 100644 --- a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts +++ b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts @@ -1,11 +1,10 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; import * as SentryCore from '@sentry/core'; import * as SentryCoreServer from '@sentry/core/server'; import * as SentryNode from '@sentry/node'; import { createTransport, NodeClient, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient, spanToJSON, @@ -101,7 +100,7 @@ describe('withServerActionInstrumentation', () => { expect.objectContaining({ name: 'getPrefecture', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', [CODE_FUNCTION_NAME]: 'getPrefecture', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', @@ -128,7 +127,7 @@ describe('withServerActionInstrumentation', () => { const span = new SentryCore.SentrySpan({ attributes: { 'http.target': '/_server', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', }, }); mockGetActiveSpan.mockReturnValue(span); @@ -153,7 +152,7 @@ describe('withServerActionInstrumentation', () => { const span = new SentryCore.SentrySpan({ attributes: { 'url.path': '/_server', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', }, }); mockGetActiveSpan.mockReturnValue(span); diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index cff338b62ed0..a0883d508e5b 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -87,6 +87,7 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 22927842682c..1730aac31865 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -53,6 +53,7 @@ export { rewriteFramesIntegration, Scope, SDK_VERSION, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index 616326565f46..c071dc2c0fb8 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import type { Client } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; import type { Load } from '@sveltejs/kit'; @@ -106,7 +106,7 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', @@ -136,7 +136,7 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -202,7 +202,7 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', @@ -223,7 +223,7 @@ describe('wrapLoadWithSentry', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index 3412e9376b25..0b258dd51ee3 100644 --- a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -1,5 +1,6 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { SpanJSON, StreamedSpanJSON, TransactionEvent } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { _enhanceKitSpan, @@ -47,7 +48,7 @@ describe('svelteKitSpansIntegration', () => { expect(event.spans).toHaveLength(1); expect(event.spans?.[0]?.op).toBe('function'); expect(event.spans?.[0]?.origin).toBe('auto.http.sveltekit'); - expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('function'); + expect(event.spans?.[0]?.data[SENTRY_OP]).toBe('function'); expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); @@ -74,7 +75,7 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBe(op); expect(span.origin).toBe(origin); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op); + expect(span.data[SENTRY_OP]).toBe(op); expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); }); @@ -88,7 +89,7 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBeUndefined(); expect(span.origin).toBeUndefined(); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(span.data[SENTRY_OP]).toBeUndefined(); expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); }); @@ -98,7 +99,7 @@ describe('svelteKitSpansIntegration', () => { op: 'http.server', origin: 'auto.http.sveltekit', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', }, span_id: '123', @@ -108,7 +109,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(rootHandleSpan); - expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(rootHandleSpan.data[SENTRY_OP]).toBe('http.server'); expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); expect(rootHandleSpan.op).toBe('http.server'); expect(rootHandleSpan.origin).toBe('auto.http.sveltekit'); @@ -118,7 +119,7 @@ describe('svelteKitSpansIntegration', () => { const span = { description: 'someOtherSpan', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + [SENTRY_OP]: 'db', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', }, op: 'db', @@ -132,7 +133,7 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBe('db'); expect(span.origin).toBe('auto.db.pg'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db'); + expect(span.data[SENTRY_OP]).toBe('db'); expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); }); @@ -142,7 +143,7 @@ describe('svelteKitSpansIntegration', () => { description: 'sveltekit.resolve', origin: 'auto.custom.origin', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SENTRY_OP]: 'custom.op', }, span_id: '123', trace_id: 'abc', @@ -152,7 +153,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(span); expect(span.origin).toBe('auto.custom.origin'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.data[SENTRY_OP]).toBe('custom.op'); }); it('overwrites previously set "manual" origins on sveltekit spans', () => { @@ -161,7 +162,7 @@ describe('svelteKitSpansIntegration', () => { description: 'sveltekit.resolve', origin: 'manual', data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SENTRY_OP]: 'custom.op', }, span_id: '123', trace_id: 'abc', @@ -171,7 +172,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(span); expect(span.origin).toBe('auto.http.sveltekit'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.data[SENTRY_OP]).toBe('custom.op'); }); }); @@ -202,7 +203,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op); + expect(span.attributes?.[SENTRY_OP]).toBe(op); expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); expect(span.attributes?.['sentry.description']).toBe(spanName); }); @@ -212,7 +213,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(span.attributes?.[SENTRY_OP]).toBeUndefined(); expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); expect(span.attributes?.['sentry.description']).toBeUndefined(); }); @@ -221,14 +222,14 @@ describe('svelteKitSpansIntegration', () => { const rootHandleSpan = makeStreamedSpan({ name: 'sveltekit.handle.root', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SENTRY_OP]: 'http.server', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', }, }); _enhanceKitSpanStreamed(rootHandleSpan); - expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(rootHandleSpan.attributes?.[SENTRY_OP]).toBe('http.server'); expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); @@ -236,14 +237,14 @@ describe('svelteKitSpansIntegration', () => { const span = makeStreamedSpan({ name: 'someOtherSpan', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + [SENTRY_OP]: 'db', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', }, }); _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db'); + expect(span.attributes?.[SENTRY_OP]).toBe('db'); expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); }); @@ -252,14 +253,14 @@ describe('svelteKitSpansIntegration', () => { const span = makeStreamedSpan({ name: 'sveltekit.resolve', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SENTRY_OP]: 'custom.op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.custom.origin', }, }); _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.attributes?.[SENTRY_OP]).toBe('custom.op'); expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.custom.origin'); }); @@ -278,14 +279,14 @@ describe('svelteKitSpansIntegration', () => { const span = makeStreamedSpan({ name: 'sveltekit.resolve', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + [SENTRY_OP]: 'custom.op', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', }, }); _enhanceKitSpanStreamed(span); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + expect(span.attributes?.[SENTRY_OP]).toBe('custom.op'); expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); }); diff --git a/packages/sveltekit/test/server-common/load.test.ts b/packages/sveltekit/test/server-common/load.test.ts index 4dc513b778c7..99433faeb3f3 100644 --- a/packages/sveltekit/test/server-common/load.test.ts +++ b/packages/sveltekit/test/server-common/load.test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; import type { Client, Event } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { NodeClient, setCurrentClient } from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; @@ -166,7 +166,7 @@ describe('wrapLoadWithSentry calls `startSpan`', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', @@ -187,7 +187,7 @@ describe('wrapLoadWithSentry calls `startSpan`', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -259,7 +259,7 @@ describe('wrapServerLoadWithSentry calls `startSpan`', () => { data: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', 'http.request.method': 'GET', 'url.path': '/users/123', @@ -301,7 +301,7 @@ describe('wrapServerLoadWithSentry calls `startSpan`', () => { expect(mockStartSpan).toHaveBeenCalledWith( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', }), name: '/users/[id]', // <-- this shows that the route was still accessed }), @@ -356,7 +356,7 @@ describe('with span streaming enabled', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', @@ -377,7 +377,7 @@ describe('with span streaming enabled', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -397,7 +397,7 @@ describe('with span streaming enabled', () => { expect(mockStartSpan).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'load', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', diff --git a/packages/sveltekit/test/server-common/serverRoute.test.ts b/packages/sveltekit/test/server-common/serverRoute.test.ts index 72efa31c1a7e..5a4564565bb8 100644 --- a/packages/sveltekit/test/server-common/serverRoute.test.ts +++ b/packages/sveltekit/test/server-common/serverRoute.test.ts @@ -1,13 +1,10 @@ +import { SENTRY_OP } from '@sentry/conventions/attributes'; import type { Client } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import type { NumericRange, RequestEvent } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - wrapServerRouteWithSentry, -} from '../../src/server'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, wrapServerRouteWithSentry } from '../../src/server'; describe('wrapServerRouteWithSentry', () => { const originalRouteHandler = vi.fn(); @@ -37,7 +34,7 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', @@ -60,7 +57,7 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', @@ -93,7 +90,7 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', @@ -115,7 +112,7 @@ describe('wrapServerRouteWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledWith( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function', + [SENTRY_OP]: 'function', 'code.function.name': 'GET', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 30d4a428eefb..6d0b6e6a0962 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -83,6 +83,7 @@ export { instrumentSupabaseClient, zodErrorsIntegration, consoleIntegration, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, From 6181072b978bb699c3c4dff3c91c96b6e4a5b851 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:50:40 +0200 Subject: [PATCH 08/73] fix(cloudflare): Detect TypeSafe Jev calls in Workers AI by model ID (#25033) ## What Workers AI calls now become `evaluate` spans only when the model ID starts with `typesafe/jev`, not when the input has `state` and `questions`. ## Why The input shape is a loose signal: any other model with those fields was traced as an evaluation. Jev is an evaluation model, so its model ID is exact, and other TypeSafe models stay unaffected. Follow-up to #24833. --------- Co-authored-by: Claude Opus 5.5 --- .../server-utils/src/ai/workers-ai/index.ts | 2 +- .../server-utils/src/ai/workers-ai/utils.ts | 17 ++++++------ .../ai/lib/utils/workers-ai-utils.test.ts | 27 ++++++++++++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/server-utils/src/ai/workers-ai/index.ts b/packages/server-utils/src/ai/workers-ai/index.ts index eb3a7c5407cb..ef49c2c4b89a 100644 --- a/packages/server-utils/src/ai/workers-ai/index.ts +++ b/packages/server-utils/src/ai/workers-ai/index.ts @@ -37,7 +37,7 @@ function instrumentRun( const [model, inputs, runOptions] = args as [unknown, unknown, Record | undefined]; - const operationName = getOperationName(inputs); + const operationName = getOperationName(model, inputs); const requestAttributes = extractRequestAttributes(model, inputs, operationName); const modelName = typeof model === 'string' && model ? model : 'unknown'; const client = getClient(); diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index 67645a456988..4af2c3424e05 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -31,10 +31,6 @@ export { setOutputMessagesAttribute }; import { WORKERS_AI_ORIGIN, WORKERS_AI_PROVIDER_NAME } from './constants'; import type { WorkersAiInput, WorkersAiOutput } from './types'; -/** - * Determine the gen_ai operation name from the inputs passed to `AI.run`. - * Workers AI exposes a single `run` method, so we infer the operation from the input shape. - */ export type WorkersAiOperationName = 'chat' | 'embeddings' | 'evaluate'; export const WORKERS_AI_OPERATION_SPAN_OPS: Record = { @@ -43,12 +39,15 @@ export const WORKERS_AI_OPERATION_SPAN_OPS: Record } { describe('workers-ai utils', () => { describe('getOperationName', () => { it('returns "chat" for prompt inputs', () => { - expect(getOperationName({ prompt: 'Hello' })).toBe('chat'); + expect(getOperationName(MODEL, { prompt: 'Hello' })).toBe('chat'); }); it('returns "chat" for messages inputs', () => { - expect(getOperationName({ messages: [{ role: 'user', content: 'Hi' }] })).toBe('chat'); + expect(getOperationName(MODEL, { messages: [{ role: 'user', content: 'Hi' }] })).toBe('chat'); }); it('returns "embeddings" for text inputs', () => { - expect(getOperationName({ text: 'embed me' })).toBe('embeddings'); + expect(getOperationName(MODEL, { text: 'embed me' })).toBe('embeddings'); }); - it('returns "evaluate" for TypeSafe state and questions inputs', () => { - expect(getOperationName({ state: 'Help!', questions: {} })).toBe('evaluate'); + it('returns "evaluate" for TypeSafe Jev models', () => { + expect(getOperationName('typesafe/jev', { state: 'Help!', questions: {} })).toBe('evaluate'); + expect(getOperationName('typesafe/jev-1.13', { state: 'Help!', questions: {} })).toBe('evaluate'); + }); + + it('does not return "evaluate" for other TypeSafe models', () => { + expect(getOperationName('typesafe/other', { prompt: 'Hello' })).toBe('chat'); + }); + + it('does not return "evaluate" for other models with state and questions inputs', () => { + expect(getOperationName(MODEL, { state: 'Help!', questions: {} })).toBe('chat'); }); it('prefers "chat" when both messages and text are present', () => { - expect(getOperationName({ messages: [{ role: 'user', content: 'Hi' }], text: 'embed me' })).toBe('chat'); + expect(getOperationName(MODEL, { messages: [{ role: 'user', content: 'Hi' }], text: 'embed me' })).toBe('chat'); }); it('falls back to "chat" for null, undefined and empty inputs', () => { - expect(getOperationName(null)).toBe('chat'); - expect(getOperationName(undefined)).toBe('chat'); - expect(getOperationName({})).toBe('chat'); + expect(getOperationName(MODEL, null)).toBe('chat'); + expect(getOperationName(MODEL, undefined)).toBe('chat'); + expect(getOperationName(MODEL, {})).toBe('chat'); }); }); From 2237750962dd4613df3107e16d852f3b4bb19134 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 10:59:53 +0200 Subject: [PATCH 09/73] test(e2e): Port Astro 5 Cloudflare to span streaming (#25016) Ports the remaining `astro-5-cf-workers` E2E app to default span streaming. `astro-7-static` retains the group's static lifecycle coverage. The server config is intentionally empty so Astro does not inject its default Node SDK initialization. The Cloudflare wrapper already initializes Sentry from Wrangler bindings, reinitializing during the first page load dropped that request's streamed span. Fixes #23809 --------- Co-authored-by: GPT-6 --- .../sentry.client.config.js | 1 - .../sentry.server.config.js | 11 +----- .../astro-5-cf-workers/tests/actions.test.ts | 30 +++++++-------- .../tests/errors.server.test.ts | 38 +++++++++---------- .../astro-5-cf-workers/wrangler.jsonc | 1 - 5 files changed, 35 insertions(+), 46 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js index e4cdc576c585..2b79ec0ed337 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.client.config.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/astro'; Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js index e4cdc576c585..afdc1e1f784e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/sentry.server.config.js @@ -1,9 +1,2 @@ -import * as Sentry from '@sentry/astro'; - -Sentry.init({ - traceLifecycle: 'static', - dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, - environment: 'qa', - tracesSampleRate: 1.0, - tunnel: 'http://localhost:3031/', // proxy server -}); +// The Worker wrapper initializes Sentry from Wrangler bindings. +// Keep this file to prevent Astro from injecting its default Node SDK initialization. diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts index 2a964941217a..e5541b27321b 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/actions.test.ts @@ -1,31 +1,29 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('Astro actions', () => { - test('captures transaction for action call', async ({ page }) => { - const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /action-test'; + test('captures span for action call', async ({ page }) => { + const spanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /action-test'; }); await page.goto('/action-test'); - const transactionEvent = await transactionEventPromise; + const span = await spanPromise; - expect(transactionEvent).toMatchObject({ - transaction: 'GET /action-test', + expect(span).toMatchObject({ + name: 'GET /action-test', }); - const traceId = transactionEvent.contexts?.trace?.trace_id; + const traceId = span.trace_id; expect(traceId).toMatch(/[a-f0-9]{32}/); }); - test('action submission creates a transaction', async ({ page }) => { + test('action submission creates a span', async ({ page }) => { await page.goto('/action-test'); - const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return ( - transactionEvent.transaction?.includes('action-test') && transactionEvent.transaction !== 'GET /action-test' - ); + const spanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'POST /_actions/[...path]'; }); await page.getByText('Submit Action').click(); @@ -36,8 +34,8 @@ test.describe('Astro actions', () => { const resultText = await page.locator('#result').textContent(); expect(resultText).toContain('success'); - const transactionEvent = await transactionEventPromise; - expect(transactionEvent).toBeDefined(); - expect(transactionEvent.contexts?.trace?.trace_id).toMatch(/[a-f0-9]{32}/); + const span = await spanPromise; + expect(span).toBeDefined(); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts index df23d740e830..4e43b4599384 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5-cf-workers/tests/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('server-side errors', () => { test('captures SSR error', async ({ page }) => { @@ -7,8 +7,8 @@ test.describe('server-side errors', () => { return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')"; }); - const transactionEventPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /ssr-error'; + const spanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /ssr-error'; }); // This page returns an error status code, so we need to catch the navigation error @@ -17,14 +17,14 @@ test.describe('server-side errors', () => { }); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const span = await spanPromise; - expect(transactionEvent).toMatchObject({ - transaction: 'GET /ssr-error', + expect(span).toMatchObject({ + name: 'GET /ssr-error', }); - const traceId = transactionEvent.contexts?.trace?.trace_id; - const spanId = transactionEvent.contexts?.trace?.span_id; + const traceId = span.trace_id; + const spanId = span.span_id; expect(traceId).toMatch(/[a-f0-9]{32}/); expect(spanId).toMatch(/[a-f0-9]{16}/); @@ -73,30 +73,30 @@ test.describe('server-side errors', () => { const errorEventPromise = waitForError('astro-5-cf-workers', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; }); - const transactionEventApiPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error/api'; + const apiSpanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /endpoint-error/api'; }); - const transactionEventEndpointPromise = waitForTransaction('astro-5-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error'; + const endpointSpanPromise = waitForStreamedSpan('astro-5-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /endpoint-error'; }); await page.goto('/endpoint-error'); await page.getByText('Get Data').click(); const errorEvent = await errorEventPromise; - const transactionEventApi = await transactionEventApiPromise; - const transactionEventEndpoint = await transactionEventEndpointPromise; + const apiSpan = await apiSpanPromise; + const endpointSpan = await endpointSpanPromise; - expect(transactionEventEndpoint).toMatchObject({ - transaction: 'GET /endpoint-error', + expect(endpointSpan).toMatchObject({ + name: 'GET /endpoint-error', }); - const traceId = transactionEventEndpoint.contexts?.trace?.trace_id; + const traceId = endpointSpan.trace_id; expect(traceId).toMatch(/[a-f0-9]{32}/); - expect(transactionEventApi).toMatchObject({ - transaction: 'GET /endpoint-error/api', + expect(apiSpan).toMatchObject({ + name: 'GET /endpoint-error/api', }); expect(errorEvent).toMatchObject({ 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 8ca6b10483d9..0b7b36047973 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 @@ -9,7 +9,6 @@ "SENTRY_ENVIRONMENT": "qa", "SENTRY_TRACES_SAMPLE_RATE": "1.0", "SENTRY_TUNNEL": "http://localhost:3031/", - "SENTRY_TRACE_LIFECYCLE": "static", }, "assets": { "binding": "ASSETS", From b139c53de577e562567f38b56d08fe99c8299e65 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 11:52:59 +0200 Subject: [PATCH 10/73] test(node): port filesystem integration tests to span streaming (#25019) Run filesystem instrumentation tests with the default span-streaming lifecycle. Preserve the existing path, error, and recording-option assertions while adapting them to streamed span fields. Each test asserts its request's span container directly. Closes #24141 (JS-3611) --------- Co-authored-by: GPT-6 --- .../instrument-record-errors-only.mjs | 1 - .../instrument-record-paths-only.mjs | 1 - .../suites/fs-instrumentation/instrument.mjs | 1 - .../suites/fs-instrumentation/test.ts | 540 +++++++++--------- 4 files changed, 279 insertions(+), 264 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs index 7043bf763c1b..eff54f5b1ba7 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-errors-only.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs index 018477190488..05beff418e87 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument-record-paths-only.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs index bc31198a2137..f17a1e3bd449 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts index 53d79a85a603..644a8c886e5e 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts @@ -1,8 +1,41 @@ +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { SENTRY_OP } from '@sentry/conventions/attributes'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../utils/runner'; +type FileSpan = Pick; + +function expectFileSpans( + segmentName: string, + expected: FileSpan[], +): (container: SerializedStreamedSpanContainer) => void { + return container => { + const spans = container.items; + const segment = spans.find(span => span.is_segment && span.name === segmentName); + expect(segment).toBeDefined(); + + const fileSpans = spans.filter( + span => span.attributes[SENTRY_OP]?.value === 'file' && span.trace_id === segment!.trace_id, + ); + expect( + fileSpans.map(({ name, status, attributes }) => ({ + name, + status, + attributes: Object.fromEntries( + Object.entries(attributes).filter( + ([key]) => + key.endsWith('_argument') || + key === 'error.type' || + key === SENTRY_OP || + key === SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + ), + ), + })), + ).toEqual(expect.arrayContaining(expected)); + }; +} + describe('fs instrumentation', () => { afterAll(() => { cleanupChildProcesses(); @@ -17,22 +50,21 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take target argument', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile-error', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'internal_error', - data: { - 'error.type': 'ENOENT', - path_argument: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /readFile-error', [ + { + name: 'fs.readFile', + status: 'error', + attributes: { + 'error.type': { type: 'string', value: 'ENOENT' }, + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -44,41 +76,38 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take one path', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /readFile', [ + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promises.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.readFile', + status: 'ok', + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file-promises.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -90,44 +119,44 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take src and dest arguments', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /copyFile', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.copyFile', - op: 'file', - status: 'ok', - data: { - src_argument: expect.stringMatching('/fixtures/some-file.txt'), - dest_argument: expect.stringMatching('/fixtures/some-file.txt.copy'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /copyFile', [ + { + name: 'fs.copyFile', + status: 'ok', + attributes: { + src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + dest_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.copy') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.copyFile', + status: 'ok', + attributes: { + src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promises.txt') }, + dest_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promises.txt.copy'), }, - }), - expect.objectContaining({ - description: 'fs.copyFile', - op: 'file', - status: 'ok', - data: { - src_argument: expect.stringMatching('/fixtures/some-file-promises.txt'), - dest_argument: expect.stringMatching('/fixtures/some-file-promises.txt.copy'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.copyFile', + status: 'ok', + attributes: { + src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promisify.txt') }, + dest_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.copy'), }, - }), - expect.objectContaining({ - description: 'fs.copyFile', - op: 'file', - status: 'ok', - data: { - src_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - dest_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.copy'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -139,44 +168,44 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take existing path and new path arguments', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /link', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.link', - op: 'file', - status: 'ok', - data: { - existing_path_argument: expect.stringMatching('/fixtures/some-file.txt'), - new_path_argument: expect.stringMatching('/fixtures/some-file.txt.link'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.link', - op: 'file', - status: 'ok', - data: { - existing_path_argument: expect.stringMatching('/some-file-promises.txt'), - new_path_argument: expect.stringMatching('/some-file-promises.txt.link'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /link', [ + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + new_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.link') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt') }, + new_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt.link') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.link', + status: 'ok', + attributes: { + existing_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.link', - op: 'file', - status: 'ok', - data: { - existing_path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - new_path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + new_path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -188,41 +217,35 @@ describe('fs instrumentation', () => { test('should create spans for fs operations that take prefix argument', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /mkdtemp', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.mkdtemp', - op: 'file', - status: 'ok', - data: { - prefix_argument: expect.stringMatching('/foo-'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.mkdtemp', - op: 'file', - status: 'ok', - data: { - prefix_argument: expect.stringMatching('/foo-'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - expect.objectContaining({ - description: 'fs.mkdtemp', - op: 'file', - status: 'ok', - data: { - prefix_argument: expect.stringMatching('/foo-'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + span: expectFileSpans('GET /mkdtemp', [ + { + name: 'fs.mkdtemp', + status: 'ok', + attributes: { + prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.mkdtemp', + status: 'ok', + attributes: { + prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.mkdtemp', + status: 'ok', + attributes: { + prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -234,44 +257,50 @@ describe('fs instrumentation', () => { test('should create spans for fs symlink operations that take target argument', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /symlink', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.symlink', - op: 'file', - status: 'ok', - data: { - target_argument: expect.stringMatching('/some-file-promisify.txt'), - path_argument: expect.stringMatching('/some-file-promisify.txt.symlink'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /symlink', [ + { + name: 'fs.symlink', + status: 'ok', + attributes: { + target_argument: { type: 'string', value: expect.stringMatching('/some-file-promisify.txt') }, + path_argument: { type: 'string', value: expect.stringMatching('/some-file-promisify.txt.symlink') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.symlink', + status: 'ok', + attributes: { + target_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), + }, + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), }, - }), - expect.objectContaining({ - description: 'fs.symlink', - op: 'file', - status: 'ok', - data: { - target_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.symlink', + status: 'ok', + attributes: { + target_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.symlink', - op: 'file', - status: 'ok', - data: { - target_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -283,31 +312,29 @@ describe('fs instrumentation', () => { test('should create spans for fs.exists callback and promisified versions', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /exists', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.exists', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /exists', [ + { + name: 'fs.exists', + status: 'ok', + attributes: { + path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + { + name: 'fs.exists', + status: 'ok', + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, - }), - expect.objectContaining({ - description: 'fs.exists', - op: 'file', - status: 'ok', - data: { - path_argument: expect.stringMatching('/fixtures/some-file-promisify.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -329,22 +356,21 @@ describe('fs instrumentation', () => { test('records file path but not error messages when only `recordFilePaths` is enabled', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile-error', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'internal_error', - // `path_argument` is recorded, but `error.type` is NOT, since `recordErrorMessagesAsSpanAttributes` is off - data: { - path_argument: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + span: expectFileSpans('GET /readFile-error', [ + { + name: 'fs.readFile', + status: 'error', + // `path_argument` is recorded, but `error.type` is NOT, since `recordErrorMessagesAsSpanAttributes` is off + attributes: { + path_argument: { + type: 'string', + value: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), }, - }), - ]), - }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -366,22 +392,18 @@ describe('fs instrumentation', () => { test('records error messages but not file paths when only `recordErrorMessagesAsSpanAttributes` is enabled', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile-error', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'internal_error', - // `error.type` is recorded, but `path_argument` is NOT, since `recordFilePaths` is off - data: { - 'error.type': 'ENOENT', - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + span: expectFileSpans('GET /readFile-error', [ + { + name: 'fs.readFile', + status: 'error', + // `error.type` is recorded, but `path_argument` is NOT, since `recordFilePaths` is off + attributes: { + 'error.type': { type: 'string', value: 'ENOENT' }, + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); @@ -393,21 +415,17 @@ describe('fs instrumentation', () => { test('does not record file paths on successful operations when only `recordErrorMessagesAsSpanAttributes` is enabled', async () => { const runner = createRunner() .expect({ - transaction: { - transaction: 'GET /readFile', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'fs.readFile', - op: 'file', - status: 'ok', - // Neither `path_argument` nor `error.type` are recorded - data: { - [SENTRY_OP]: 'file', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', - }, - }), - ]), - }, + span: expectFileSpans('GET /readFile', [ + { + name: 'fs.readFile', + status: 'ok', + // Neither `path_argument` nor `error.type` are recorded + attributes: { + [SENTRY_OP]: { type: 'string', value: 'file' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + }, + }, + ]), }) .start(); From e74d7cfd419915ae43a2ba22e605da92e4e53c66 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 12:19:45 +0200 Subject: [PATCH 11/73] ref(browser): Use SENTRY_ORIGIN convention constant (#25037) Uses `SENTRY_ORIGIN` from `@sentry/conventions/attributes` in the browser SDK, browser framework integrations, and their tests. It has the same `sentry.origin` value; transmitted data and public exports are unchanged. Part of #24961 / [JS-3868](https://linear.app/getsentry/issue/JS-3868). Co-authored-by: GPT-6 --- .../suites/public-api/startSpan/basic/test.ts | 6 +-- .../public-api/startSpan/streamed/test.ts | 11 +++-- .../test.ts | 7 ++- .../long-animation-frame-enabled/test.ts | 7 ++- .../test.ts | 7 ++- .../test.ts | 7 ++- .../navigation-aborting-pageload/test.ts | 8 ++-- .../multiple-redirects/test.ts | 12 +++-- .../pageload-redirect/test.ts | 12 +++-- .../navigation-streamed/test.ts | 9 ++-- .../navigation/test.ts | 12 ++--- .../pageload-streamed/test.ts | 5 +- .../pageload-update-txn-name/test.ts | 6 +-- .../pageload-updateSpanName/test.ts | 6 +-- .../pageload/test.ts | 6 +-- .../reportPageLoaded-streamed/default/test.ts | 11 +++-- .../finalTimeout/test.ts | 6 +-- .../navigation/test.ts | 6 +-- .../reportPageLoaded/default/test.ts | 6 +-- .../reportPageLoaded/finalTimeout/test.ts | 6 +-- .../reportPageLoaded/navigation/test.ts | 6 +-- .../suites/tracing/interactions/spans/test.ts | 7 +-- .../metrics/pageload-resource-spans/test.ts | 8 ++-- .../angular-19/tests/performance.test.ts | 18 ++++---- packages/angular/src/tracing.ts | 14 +++--- packages/angular/test/tracing.test.ts | 14 ++++-- .../integrations/fetchStreamPerformance.ts | 5 +- packages/browser/src/tracing/request.ts | 4 +- .../tracing/bfcacheRestoreNavigation.test.ts | 6 +-- .../tracing/browserTracingIntegration.test.ts | 37 ++++++++------- ...nstrumentEmberAppInstanceForPerformance.ts | 9 ++-- .../ember/src/utils/instrumentEmberGlobals.ts | 10 ++-- .../src/utils/instrumentRoutePerformance.ts | 6 +-- packages/react/src/profiler.tsx | 13 +++--- .../instrumentation.tsx | 7 ++- packages/react/src/reactrouter.tsx | 7 ++- packages/react/src/reactrouterv3.ts | 13 ++---- packages/react/src/tanstackrouter.ts | 6 +-- packages/react/test/react-router.test.tsx | 8 ++-- .../test/reactrouter-cross-usage.test.tsx | 10 ++-- .../reactrouter-descendant-routes.test.tsx | 14 +++--- packages/react/test/reactrouterv3.test.tsx | 14 +++--- packages/react/test/reactrouterv4.test.tsx | 22 ++++----- packages/react/test/reactrouterv5.test.tsx | 22 ++++----- packages/react/test/reactrouterv6.test.tsx | 46 +++++++++---------- packages/react/test/tanstackrouter.test.ts | 5 +- packages/solid/src/solidrouter.ts | 11 ++--- packages/solid/src/tanstackrouter.ts | 6 +-- packages/solid/test/solidrouter.test.tsx | 12 ++--- packages/solid/test/tanstackrouter.test.ts | 5 +- packages/svelte/src/performance.ts | 7 ++- packages/vue/src/router.ts | 6 +-- packages/vue/src/tanstackrouter.ts | 6 +-- packages/vue/src/tracing.ts | 8 ++-- packages/vue/test/router.test.ts | 20 ++++---- packages/vue/test/tanstackrouter.test.ts | 7 ++- 56 files changed, 294 insertions(+), 281 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index 326c05a787f4..824220e5133f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -21,7 +21,7 @@ sentryTest( const attributes = transaction.contexts?.trace?.data; expect(attributes).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts index e10826f486a9..5cc3bd34f817 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; @@ -15,6 +15,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; sentryTest( @@ -77,7 +78,7 @@ sentryTest( type: 'string', value: 'test-child', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -122,7 +123,7 @@ sentryTest( { attributes: { 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -167,7 +168,7 @@ sentryTest( { attributes: { 'sentry.is_localhost': { value: false, type: 'boolean' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -240,7 +241,7 @@ sentryTest( type: 'string', value: 'test', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts index 5f15f4354416..f885fc4c3bde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled-streamed/test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; @@ -46,7 +45,7 @@ sentryTest( }, 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); @@ -95,7 +94,7 @@ sentryTest('captures long animation frame span for event listener.', async ({ br 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts index a6ed74270ebb..937662e117a4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-animation-frame-enabled/test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -46,7 +45,7 @@ sentryTest( 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', 'browser.script.invoker_type': 'classic-script', [SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); @@ -100,7 +99,7 @@ sentryTest( 'browser.script.invoker_type': 'event-listener', 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', [SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts index 4a4cda50b962..94ece2b74e6f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled-streamed/test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; @@ -48,7 +47,7 @@ sentryTest( }, 'browser.script.invoker_type': { type: 'string', value: 'classic-script' }, [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); @@ -97,7 +96,7 @@ sentryTest('captures long animation frame span for event listener.', async ({ br 'browser.script.invoker_type': { type: 'string', value: 'event-listener' }, 'code.file.path': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, [SENTRY_OP]: { type: 'string', value: 'ui.long_animation_frame' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.ui.browser.metrics' }, }), }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts index 5efc2258f48f..b571a7d6a306 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-and-animation-frame-enabled/test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -48,7 +47,7 @@ sentryTest( 'browser.script.invoker': 'https://sentry-test-site.example/path/to/script.js', 'browser.script.invoker_type': 'classic-script', [SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); @@ -102,7 +101,7 @@ sentryTest( 'browser.script.invoker_type': 'event-listener', 'code.file.path': 'https://sentry-test-site.example/path/to/script.js', [SENTRY_OP]: 'ui.long_animation_frame', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }), ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts index f58116dd9c32..829710ce5011 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; @@ -40,14 +40,14 @@ sentryTest( expect(navigationRequest.transaction).toEqual('/sub-page'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'cancelled', }); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'navigation', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts index 9fd5b0cdc35f..caa2b6afb328 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/multiple-redirects/test.ts @@ -1,8 +1,14 @@ import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; sentryTest( 'creates a pageload and navigation root spans each with multiple navigation.redirect childspans', @@ -27,7 +33,7 @@ sentryTest( expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts index 0735f69f127c..fc126da536f3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/pageload-redirect/test.ts @@ -1,8 +1,14 @@ import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; sentryTest('creates a pageload root span with navigation.redirect childspan', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -20,7 +26,7 @@ sentryTest('creates a pageload root span with navigation.redirect childspan', as expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts index b2281d6ddfa4..62fcd88c337f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_TRACE_LIFECYCLE, @@ -9,6 +9,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -124,7 +125,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow type: 'string', value: 'navigation', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser', }, @@ -220,7 +221,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: '/sub-page', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser', }, @@ -249,7 +250,7 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page }) type: 'string', value: '/sub-page-2', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.navigation.browser', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts index a7cf07af6838..ab8a3931e132 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation/test.ts @@ -1,7 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, @@ -37,14 +37,14 @@ sentryTest('should create a navigation transaction on page navigation', async ({ expect(navigationRequest.transaction).toEqual('/index.html'); expect(pageloadRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', ['sentry.idle_span_finish_reason']: 'idleTimeout', }); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'navigation', @@ -115,7 +115,7 @@ sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, pa expect(navigationRequest.transaction).toEqual('/sub-page'); expect(navigationRequest.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'navigation', @@ -135,7 +135,7 @@ sentryTest('should handle pushState with full URL', async ({ getLocalTestUrl, pa expect(navigationRequest2.transaction).toEqual('/sub-page-2'); expect(navigationRequest2.contexts?.trace?.data).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'navigation', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts index e7127322f2a8..717011ffb9c3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { BROWSER_NAVIGATION_TYPE, SENTRY_SEGMENT_NAME_SOURCE, @@ -14,6 +14,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -136,7 +137,7 @@ sentryTest( type: 'string', value: 'pageload', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts index 5bb181132068..0776668fb9e8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -23,7 +23,7 @@ sentryTest( expect(eventData.transaction).toBe('new name'); expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts index 3a3952d1637b..ab1b1ed20c46 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -21,7 +21,7 @@ sentryTest( expect(traceContextData).toBeDefined(); expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts index b1aa7b48bb0d..8e7c3a4b2480 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -22,7 +22,7 @@ sentryTest('creates a pageload transaction with url as source', async ({ getLoca expect(startTimestamp).toBeCloseTo(timeOrigin, 1); expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts index b5f733ff6298..3c4a37bc9de3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/default/test.ts @@ -1,6 +1,11 @@ -import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_IDLE_SPAN_FINISH_REASON, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -21,7 +26,7 @@ sentryTest( const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; expect(pageloadSpan.attributes).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, [SENTRY_OP]: { type: 'string', value: 'pageload' }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts index d7da4c7c3cc6..8ea2ea1ca842 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/finalTimeout/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -21,7 +21,7 @@ sentryTest( const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; expect(pageloadSpan.attributes).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, [SENTRY_OP]: { type: 'string', value: 'pageload' }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts index b224fa51fdb3..db6f6737ccd6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded-streamed/navigation/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils'; @@ -21,7 +21,7 @@ sentryTest( const spanDurationSeconds = pageloadSpan.end_timestamp - pageloadSpan.start_timestamp; expect(pageloadSpan.attributes).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.pageload.browser' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: expect.objectContaining({ value: 1 }), [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'url' }, [SENTRY_OP]: { type: 'string', value: 'pageload' }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts index 8448aad75624..bca0d6052b95 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/default/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -23,7 +23,7 @@ sentryTest( const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts index 0bcc574b5933..ae6ffdec21b3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/finalTimeout/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -23,7 +23,7 @@ sentryTest( const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts index c3de6c158f9c..9e947592a576 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/reportPageLoaded/navigation/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser'; import { sentryTest } from '../../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers'; @@ -23,7 +23,7 @@ sentryTest( const spanDurationSeconds = eventData.timestamp! - eventData.start_timestamp!; expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts index 15cf43e68b5e..0870974ba825 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_ID, @@ -13,6 +13,7 @@ import { SENTRY_ENVIRONMENT, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -81,7 +82,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'ui.action.click', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.browser.interactions', }, @@ -142,7 +143,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: expect.any(String), }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.browser.interactions', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 3b8f662f0003..276234385fae 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,7 +1,7 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { type Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -97,7 +97,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU 'network.protocol.name': '', 'network.protocol.version': 'unknown', [SENTRY_OP]: 'resource.img', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'sentry-test-site.example', 'url.domain': 'sentry-test-site.example', 'http.request.same_origin': false, @@ -150,7 +150,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU 'network.protocol.name': '', 'network.protocol.version': 'unknown', [SENTRY_OP]: 'resource.link', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'sentry-test-site.example', 'url.domain': 'sentry-test-site.example', 'http.request.same_origin': false, diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts index a9e2b94e214e..b78206f5566c 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -1,8 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -// Cannot use @sentry/angular here due to build stuff -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; // The `angular-19 (streamed)` variant builds the app with `traceLifecycle: 'stream'`, which emits // spans instead of transactions. See `streamed-performance.test.ts` for that variant. @@ -258,7 +256,7 @@ test.describe('TraceDirective', () => { const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); const traceDirectiveSpans = navigationTxn.spans?.filter( - span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', + span => span?.data && span?.data[SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', ); expect(traceDirectiveSpans).toHaveLength(2); @@ -267,7 +265,7 @@ test.describe('TraceDirective', () => { expect.objectContaining({ data: { [SENTRY_OP]: 'ui.mount', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', 'ui.component_name': 'sample-component', }, description: '', // custom component name passed to trace directive @@ -279,7 +277,7 @@ test.describe('TraceDirective', () => { expect.objectContaining({ data: { [SENTRY_OP]: 'ui.mount', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', 'ui.component_name': 'app-sample-component', }, description: '', // fallback selector name @@ -305,7 +303,7 @@ test.describe('TraceClass Decorator', () => { const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); const classDecoratorSpan = navigationTxn.spans?.find( - span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', + span => span?.data && span?.data[SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', ); expect(classDecoratorSpan).toBeDefined(); @@ -313,7 +311,7 @@ test.describe('TraceClass Decorator', () => { expect.objectContaining({ data: { [SENTRY_OP]: 'ui.mount', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', 'ui.component_name': 'ComponentTrackingComponent', }, description: '', @@ -346,7 +344,7 @@ test.describe('TraceMethod Decorator', () => { expect.objectContaining({ data: { [SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', 'code.function.name': 'ngOnInit', }, description: '', @@ -377,7 +375,7 @@ test.describe('TraceMethod Decorator', () => { expect.objectContaining({ data: { [SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', 'code.function.name': 'ngAfterViewInit', }, description: '', diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 553da897f9e2..3a50bf232339 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -15,7 +15,6 @@ import { getClient, getCurrentScope, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, startBrowserTracingNavigationSpan, startInactiveSpan, @@ -30,6 +29,7 @@ import { URL_TEMPLATE, SENTRY_DESCRIPTION, UI_COMPONENT_NAME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, ROUTER } from '@sentry/conventions/op'; import type { Integration, Span } from '@sentry/core'; @@ -90,7 +90,7 @@ export function _updateSpanAttributesForParametrizedUrl(route: string, url: stri const absoluteUrl = getAbsoluteUrl(url); span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.${attributes[SENTRY_OP]}.angular`, + [SENTRY_ORIGIN]: `auto.${attributes[SENTRY_OP]}.angular`, [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_FULL]: filterCollectedUrl(absoluteUrl), [URL_PATH]: parseStringToURLObject(absoluteUrl)?.pathname, @@ -133,7 +133,7 @@ export class TraceService implements OnDestroy { // is only known on `ResolveEnd`, which updates the span name then. name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : strippedUrl, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.angular', + [SENTRY_ORIGIN]: 'auto.navigation.angular', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }, @@ -157,7 +157,7 @@ export class TraceService implements OnDestroy { name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : `${navigationEvent.url}`, attributes: { [SENTRY_OP]: ROUTER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular', + [SENTRY_ORIGIN]: 'auto.ui.angular', [URL_FULL]: strippedUrl, ...(navigationEvent.navigationTrigger && { navigationTrigger: navigationEvent.navigationTrigger, @@ -324,7 +324,7 @@ export class TraceDirective implements OnInit, AfterViewInit { name: hasSpanStreaming ? componentName : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', [UI_COMPONENT_NAME]: componentName, ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -382,7 +382,7 @@ export function TraceClass(options?: TraceClassOptions): ClassDecorator { name: hasSpanStreaming ? componentName || UI_MOUNT_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -440,7 +440,7 @@ export function TraceMethod(options?: TraceMethodOptions): MethodDecorator { startTime: now, attributes: { [SENTRY_OP]: FUNCTION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + [SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', // override description inference by Relay to preserve the original (transaction-based) description. // sentry-conventions can't map the special case with the angle brackets. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 13acbc9d8622..3139125c36db 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,10 +1,16 @@ import { ElementRef } from '@angular/core'; import type { ActivatedRouteSnapshot } from '@angular/router'; -import { getMainCarrier, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentrySpan, spanToJSON, startSpan } from '@sentry/core'; +import { getMainCarrier, SentrySpan, spanToJSON, startSpan } from '@sentry/core'; import { describe, it } from 'vitest'; import { browserTracingIntegration, init, TraceClass, TraceDirective } from '../src/index'; import { _updateSpanAttributesForParametrizedUrl, getParameterizedRouteFromSnapshot } from '../src/tracing'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { expect } from 'vitest'; describe('browserTracingIntegration', () => { @@ -81,7 +87,7 @@ describe('Angular Tracing', () => { expect(spanToJSON(span)).toEqual( expect.objectContaining({ attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.undefined.angular', + [SENTRY_ORIGIN]: 'auto.undefined.angular', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: route, // URL_FULL is resolved against jsdom's http://localhost origin @@ -106,7 +112,7 @@ describe('Angular Tracing', () => { expect(spanToJSON(span)).toEqual( expect.objectContaining({ attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SENTRY_SEGMENT_NAME_SOURCE]: 'sample-source', }, name: 'initial-span-name', diff --git a/packages/browser/src/integrations/fetchStreamPerformance.ts b/packages/browser/src/integrations/fetchStreamPerformance.ts index 2a5c0dc9c103..c021c959e883 100644 --- a/packages/browser/src/integrations/fetchStreamPerformance.ts +++ b/packages/browser/src/integrations/fetchStreamPerformance.ts @@ -1,4 +1,4 @@ -import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes'; +import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_DOMAIN, URL_FULL, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_CLIENT_STREAM } from '@sentry/conventions/op'; import type { IntegrationFn, Span } from '@sentry/core'; import { @@ -9,7 +9,6 @@ import { getUrlDomain, hasSpanStreamingEnabled, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, stripDataUrlContent, filterCollectedUrl, } from '@sentry/core'; @@ -93,7 +92,7 @@ export const fetchStreamPerformanceIntegration = defineIntegration(() => { [HTTP_REQUEST_METHOD]: method, type: 'fetch', [SENTRY_OP]: HTTP_CLIENT_STREAM, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.stream', + [SENTRY_ORIGIN]: 'auto.http.browser.stream', }, }); diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 2320f12cba6f..dd5cdb351a3c 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -13,7 +13,6 @@ import { instrumentFetchRequest, matchesTracePropagationTargets, parseUrl, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, setHttpStatus, spanIsIgnored, @@ -44,6 +43,7 @@ import { URL_FRAGMENT, URL_FULL, URL_QUERY, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; @@ -393,7 +393,7 @@ function xhrCallback( [URL_FULL]: filterCollectedUrl(sanitizedFullUrl), [SERVER_ADDRESS]: host, [URL_DOMAIN]: domain, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', + [SENTRY_ORIGIN]: 'auto.http.browser', [SENTRY_OP]: HTTP_CLIENT, [URL_QUERY]: filterCollectedUrlQuery(getUrlQuery(parsedUrl?.search)), [URL_FRAGMENT]: getUrlFragment(parsedUrl?.hash), diff --git a/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts b/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts index 27cc3089b309..d2b542a5392f 100644 --- a/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts +++ b/packages/browser/test/tracing/bfcacheRestoreNavigation.test.ts @@ -2,11 +2,11 @@ * @vitest-environment jsdom */ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { browserPerformanceTimeOrigin, getActiveSpan, getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient, spanToJSON, } from '@sentry/core'; @@ -66,7 +66,7 @@ describe('bfcache restore, then the first history navigation of the document', ( WINDOW.dispatchEvent(event); expect(spanToJSON(getActiveSpan()!).attributes).toEqual( - expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), + expect.objectContaining({ [SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), ); // Past the redirect threshold, so the navigation is judged on the pageload guard alone rather @@ -75,7 +75,7 @@ describe('bfcache restore, then the first history navigation of the document', ( WINDOW.history.pushState({}, '', '/after-restore'); expect(spanToJSON(getActiveSpan()!).attributes).toEqual( - expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser' }), + expect.objectContaining({ [SENTRY_ORIGIN]: 'auto.navigation.browser' }), ); }); }); diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 38bc3e745045..0061ec0dde23 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -9,7 +9,6 @@ import { getDynamicSamplingContextFromSpan, getMainCarrier, metrics, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, spanIsSampled, @@ -34,7 +33,13 @@ import { PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE } from '../../src/tracing/linkedTrace import { bfcacheMetricsIntegration } from '../../src/integrations/bfcacheMetrics'; import * as webVitalsModule from '../../src/integrations/webVitals'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + URL_FULL, + URL_PATH, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; const oldTextEncoder = global.window.TextEncoder; const oldTextDecoder = global.window.TextDecoder; @@ -176,7 +181,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/', @@ -320,7 +325,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/', @@ -355,7 +360,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/test', @@ -398,7 +403,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/test2', @@ -443,7 +448,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: 'https://example.com/', @@ -478,7 +483,7 @@ describe('browserTracingIntegration', () => { expect.objectContaining({ attributes: { [SENTRY_OP]: 'navigation.redirect', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser', + [SENTRY_ORIGIN]: 'auto.navigation.browser', [URL_FULL]: 'https://example.com/test', [URL_PATH]: '/test', }, @@ -539,7 +544,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [URL_FULL]: 'https://example.com/', @@ -605,7 +610,7 @@ describe('browserTracingIntegration', () => { name: 'test span', attributes: { testy: 'yes', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }); @@ -615,7 +620,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [URL_FULL]: 'https://example.com/', @@ -917,7 +922,7 @@ describe('browserTracingIntegration', () => { expect(spanToJSON(span).attributes).toEqual( expect.objectContaining({ [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache', + [SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache', 'browser.navigation.type': 'back-forward-cache', }), ); @@ -954,7 +959,7 @@ describe('browserTracingIntegration', () => { firePageShow(true); expect(spanToJSON(getActiveSpan()!).attributes).toEqual( - expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), + expect.objectContaining({ [SENTRY_ORIGIN]: 'auto.navigation.browser.bfcache' }), ); }); @@ -1045,7 +1050,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [PREVIOUS_TRACE_TMP_SPAN_ATTRIBUTE]: expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/), @@ -1092,7 +1097,7 @@ describe('browserTracingIntegration', () => { name: 'test span', attributes: { testy: 'yes', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }); @@ -1102,7 +1107,7 @@ describe('browserTracingIntegration', () => { status: 'ok', attributes: { [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', [URL_FULL]: 'https://example.com/', diff --git a/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts b/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts index d598c41fa834..6f3df88fab75 100644 --- a/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts +++ b/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts @@ -5,7 +5,7 @@ import type { startBrowserTracingNavigationSpan as startBrowserTracingNavigationSpanType, startBrowserTracingPageLoadSpan as startBrowserTracingPageLoadSpanType, } from '@sentry/browser'; -import { getAbsoluteUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, WINDOW } from '@sentry/browser'; +import { getAbsoluteUrl, startInactiveSpan, WINDOW } from '@sentry/browser'; import { ROUTER_NAVIGATION_ROUTE_ID, SENTRY_SEGMENT_NAME_SOURCE, @@ -13,6 +13,7 @@ import { URL_FULL, URL_PATH, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { ROUTER } from '@sentry/conventions/op'; import { @@ -77,7 +78,7 @@ export function instrumentEmberAppInstanceForPerformance( : url || WINDOW.location.pathname, attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: routeInfo ? 'route' : 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.ember', + [SENTRY_ORIGIN]: 'auto.pageload.ember', ...(routeInfo?.name && { [ROUTER_NAVIGATION_ROUTE_ID]: routeInfo.name }), ...(url ? _getRouteUrlAttributes(client, url, routeInfo?.params) : {}), toRoute: routeInfo?.name, @@ -121,7 +122,7 @@ export function instrumentEmberAppInstanceForPerformance( name: transactionName, attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.ember', + [SENTRY_ORIGIN]: 'auto.navigation.ember', ...(toRoute && { [ROUTER_NAVIGATION_ROUTE_ID]: toRoute }), ...urlAttributes, fromRoute, @@ -154,7 +155,7 @@ export function instrumentEmberAppInstanceForPerformance( transitionSpan = startInactiveSpan({ attributes: { [SENTRY_OP]: ROUTER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', }, // With span streaming, span names have to be low cardinality, and Ember gives us no route // template for the transition itself, so it's the fallback. diff --git a/packages/ember/src/utils/instrumentEmberGlobals.ts b/packages/ember/src/utils/instrumentEmberGlobals.ts index 498953a062f1..31c76d10af51 100644 --- a/packages/ember/src/utils/instrumentEmberGlobals.ts +++ b/packages/ember/src/utils/instrumentEmberGlobals.ts @@ -1,8 +1,8 @@ import { subscribe } from '@ember/instrumentation'; import { scheduleOnce } from '@ember/runloop'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_RESOLVE, UI_TASK } from '@sentry/conventions/op'; -import { getActiveSpan, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; +import { getActiveSpan, getClient, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { browserPerformanceTimeOrigin, @@ -101,7 +101,7 @@ function _instrumentEmberRunloop(config: { minimumRunloopQueueDuration?: number startInactiveSpan({ attributes: { [SENTRY_OP]: UI_TASK, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', 'ember.runloop.queue': queue, ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -168,7 +168,7 @@ export function _processComponentRenderAfter( startTime: begin.now, attributes: { [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', [UI_COMPONENT_NAME]: name, }, onlyIfParent: true, @@ -253,7 +253,7 @@ function _instrumentInitialLoad(): void { name: hasSpanStreaming ? UI_MOUNT_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, startTime, diff --git a/packages/ember/src/utils/instrumentRoutePerformance.ts b/packages/ember/src/utils/instrumentRoutePerformance.ts index 1515c7ed3f90..f7c2bdd77eac 100644 --- a/packages/ember/src/utils/instrumentRoutePerformance.ts +++ b/packages/ember/src/utils/instrumentRoutePerformance.ts @@ -1,7 +1,7 @@ import { startSpan } from '@sentry/browser'; -import { CODE_FUNCTION_NAME, SENTRY_DESCRIPTION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, SENTRY_DESCRIPTION, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; -import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled } from '@sentry/core'; import type Route from '@ember/routing/route'; @@ -43,7 +43,7 @@ export function instrumentRoutePerformance(BaseRoute return startSpan( { attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember', + [SENTRY_ORIGIN]: 'auto.ui.ember', [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: hookName, ...(isStreaming && { [SENTRY_DESCRIPTION]: fullRouteName }), diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 7f2e07aaf9ec..508d9e508bdf 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -2,7 +2,6 @@ import { getClient, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, timestampInSeconds, UI_MOUNT_SPAN_NAME_FALLBACK, @@ -10,7 +9,7 @@ import { UI_UPDATE_SPAN_NAME_FALLBACK, withActiveSpan, } from '@sentry/core'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_UPDATE } from '@sentry/conventions/op'; import * as React from 'react'; import { hoistNonReactStatics } from './hoist-non-react-statics'; @@ -66,7 +65,7 @@ class Profiler extends React.Component { onlyIfParent: true, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -102,7 +101,7 @@ class Profiler extends React.Component { startTime: now, attributes: { [SENTRY_OP]: UI_UPDATE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), 'ui.react.changed_props': changedProps, ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), @@ -142,7 +141,7 @@ class Profiler extends React.Component { startTime, attributes: { [SENTRY_OP]: UI_RENDER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -229,7 +228,7 @@ function useProfiler( onlyIfParent: true, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -257,7 +256,7 @@ function useProfiler( startTime, attributes: { [SENTRY_OP]: UI_RENDER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.react.profiler', + [SENTRY_ORIGIN]: 'auto.ui.react.profiler', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, diff --git a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx index ff860013dd59..629b839d11ad 100644 --- a/packages/react/src/reactrouter-compat-utils/instrumentation.tsx +++ b/packages/react/src/reactrouter-compat-utils/instrumentation.tsx @@ -18,7 +18,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import * as React from 'react'; @@ -49,7 +48,7 @@ import { setNavigationContext, transactionNameHasWildcard, } from './utils'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; const reactRouterConfigByClient = new WeakMap(); @@ -756,7 +755,7 @@ export function createReactRouterV6CompatibleTracingIntegration( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.reactrouter${version ? `_v${version}` : ''}`, + [SENTRY_ORIGIN]: `auto.pageload.react.reactrouter${version ? `_v${version}` : ''}`, }, }); } @@ -1078,7 +1077,7 @@ export function handleNavigation(opts: { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: source, [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.reactrouter${version ? `_v${version}` : ''}`, + [SENTRY_ORIGIN]: `auto.navigation.react.reactrouter${version ? `_v${version}` : ''}`, ...(source === 'route' && { [URL_TEMPLATE]: placeholderEntry.routeName }), }, }); diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 2dbc01907fe6..c843c6cd3a43 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -12,14 +12,13 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import type { ReactElement } from 'react'; import * as React from 'react'; import { hoistNonReactStatics } from './hoist-non-react-statics'; import type { Action, Location } from './types'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; // We need to disable eslint no-explicit-any because any is required for the @@ -164,7 +163,7 @@ function instrumentReactRouter( name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : PAGELOAD_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`, + [SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`, [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: name }), }, @@ -181,7 +180,7 @@ function instrumentReactRouter( name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`, + [SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`, [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: name }), }, diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index 7e799b742aea..501c13f0d6bb 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -5,14 +5,9 @@ import { WINDOW, } from '@sentry/browser'; import type { Integration, TransactionSource } from '@sentry/core'; -import { - hasSpanStreamingEnabled, - NAVIGATION_SPAN_NAME_FALLBACK, - PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK } from '@sentry/core'; import type { Location } from './types'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; // Many of the types below had to be mocked out to prevent typescript issues @@ -70,7 +65,7 @@ export function reactRouterV3BrowserTracingIntegration( name: source === 'route' || !hasSpanStreamingEnabled(client) ? localName : PAGELOAD_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: localName }), }, @@ -93,7 +88,7 @@ export function reactRouterV3BrowserTracingIntegration( source === 'route' || !hasSpanStreamingEnabled(client) ? localName : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: localName }), }, diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 3d4007d7d155..41eec4694aae 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -12,7 +12,6 @@ import { NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { VendoredTanstackRouter, VendoredTanstackRouterRouteMatch } from './vendor/tanstackrouter-types'; import { PARAMS_KEY_BASE, @@ -22,6 +21,7 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; @@ -104,7 +104,7 @@ export function tanstackRouterBrowserTracingIntegration( : initialWindowLocation.pathname, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), @@ -161,7 +161,7 @@ export function tanstackRouterBrowserTracingIntegration( name: routeMatch ? routeMatch.routeId : fallbackName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.react.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), diff --git a/packages/react/test/react-router.test.tsx b/packages/react/test/react-router.test.tsx index 7422c57878da..b25323e691c2 100644 --- a/packages/react/test/react-router.test.tsx +++ b/packages/react/test/react-router.test.tsx @@ -5,8 +5,8 @@ * directly from `react` / `react-router` so `reactRouterBrowserTracingIntegration()` can be used * without passing them in. */ -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; import { MemoryRouter, Route, Routes, useNavigate } from 'react-router'; @@ -74,7 +74,7 @@ describe('@sentry/react/react-router', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', // version-agnostic origin (no `_v6`/`_v7` suffix) - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter', }, }); expect(getCurrentScope().getScopeData().transactionName).toEqual('/about'); @@ -115,7 +115,7 @@ describe('@sentry/react/react-router', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter', }, }); }); diff --git a/packages/react/test/reactrouter-cross-usage.test.tsx b/packages/react/test/reactrouter-cross-usage.test.tsx index 6b312e8987ba..580cd11841df 100644 --- a/packages/react/test/reactrouter-cross-usage.test.tsx +++ b/packages/react/test/reactrouter-cross-usage.test.tsx @@ -1,8 +1,8 @@ /** * @vitest-environment jsdom */ -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { render, waitFor } from '@testing-library/react'; import * as React from 'react'; import { act } from 'react'; @@ -662,7 +662,7 @@ describe('React Router cross usage of wrappers', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/settings', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -781,7 +781,7 @@ describe('React Router cross usage of wrappers', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/user/:id', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); @@ -803,7 +803,7 @@ describe('React Router cross usage of wrappers', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/user/:id', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); diff --git a/packages/react/test/reactrouter-descendant-routes.test.tsx b/packages/react/test/reactrouter-descendant-routes.test.tsx index 4754f55f4e55..7f712ad0f5d7 100644 --- a/packages/react/test/reactrouter-descendant-routes.test.tsx +++ b/packages/react/test/reactrouter-descendant-routes.test.tsx @@ -1,8 +1,8 @@ /** * @vitest-environment jsdom */ -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -172,7 +172,7 @@ describe('React Router Descendant Routes', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/child/:id', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -304,7 +304,7 @@ describe('React Router Descendant Routes', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/:orgId/:id', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -415,7 +415,7 @@ describe('React Router Descendant Routes', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -476,7 +476,7 @@ describe('React Router Descendant Routes', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -629,7 +629,7 @@ describe('React Router Descendant Routes', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId/:detailId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index 24546d9eeffa..29f54903d579 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -2,8 +2,8 @@ * @vitest-environment jsdom */ import { BrowserClient } from '@sentry/browser'; -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { render } from '@testing-library/react'; import * as React from 'react'; import { act } from 'react'; @@ -102,7 +102,7 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v3', [SENTRY_OP]: 'pageload', }, }); @@ -139,7 +139,7 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SENTRY_OP]: 'navigation', }, }); @@ -153,7 +153,7 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/features', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SENTRY_OP]: 'navigation', }, }); @@ -195,7 +195,7 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/:userid', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SENTRY_OP]: 'navigation', }, }); @@ -211,7 +211,7 @@ describe('browserTracingReactRouterV3', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/teams/:teamId/details', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v3', [SENTRY_OP]: 'navigation', }, }); diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index c1d6bff53432..157e0e43efeb 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,14 +1,14 @@ /** * @vitest-environment jsdom */ -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-4'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { BrowserClient, reactRouterV4BrowserTracingIntegration, withSentryRouting } from '../src'; import type { RouteConfig } from '../src/reactrouter'; @@ -80,7 +80,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v4', [SENTRY_OP]: 'pageload', }, }); @@ -125,7 +125,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -138,7 +138,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -197,7 +197,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -234,7 +234,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -279,7 +279,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -302,7 +302,7 @@ describe('browserTracingReactRouterV4', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -351,7 +351,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/organizations/:orgid/v1/:teamid', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); @@ -365,7 +365,7 @@ describe('browserTracingReactRouterV4', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/organizations/:orgid', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v4', [SENTRY_OP]: 'navigation', }, }); diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 7e25b9fbb4bc..a36474c9a5a0 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,14 +1,14 @@ /** * @vitest-environment jsdom */ -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; import * as React from 'react'; import { act } from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-5'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { BrowserClient, reactRouterV5BrowserTracingIntegration, withSentryRouting } from '../src'; import type { RouteConfig } from '../src/reactrouter'; @@ -80,7 +80,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Pageload', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v5', [SENTRY_OP]: 'pageload', }, }); @@ -125,7 +125,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', }, }); @@ -138,7 +138,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', }, }); @@ -197,7 +197,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', }, }); @@ -234,7 +234,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', }, }); @@ -279,7 +279,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', }, }); @@ -302,7 +302,7 @@ describe('browserTracingReactRouterV5', () => { name: 'Navigation', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', }, }); @@ -350,7 +350,7 @@ describe('browserTracingReactRouterV5', () => { name: '/organizations/:orgid/v1/:teamid', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', [URL_TEMPLATE]: '/organizations/:orgid/v1/:teamid', }, @@ -364,7 +364,7 @@ describe('browserTracingReactRouterV5', () => { name: '/organizations/:orgid', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v5', [SENTRY_OP]: 'navigation', [URL_TEMPLATE]: '/organizations/:orgid', }, diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index d22f2e445d49..fc9e7e664644 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,8 +1,8 @@ /** * @vitest-environment jsdom */ -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; import type { RouteObject } from 'react-router-6'; @@ -209,7 +209,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -256,7 +256,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); @@ -293,7 +293,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); }); @@ -411,7 +411,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -498,7 +498,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/us', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -536,7 +536,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/:page', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -576,7 +576,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/stores/:storeId/products/:productId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -624,7 +624,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -674,7 +674,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -726,7 +726,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -766,7 +766,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -806,7 +806,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -878,7 +878,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', }, }); }); @@ -1030,7 +1030,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1082,7 +1082,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/us', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1134,7 +1134,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/about/:page', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1192,7 +1192,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/stores/:storeId/products/:productId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1274,7 +1274,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/projects/:projectId/views/:viewId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1354,7 +1354,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/param-page/:id/details/:superId', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1511,7 +1511,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); @@ -1551,7 +1551,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/issues/:groupId/', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + [SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', }, }); }); diff --git a/packages/react/test/tanstackrouter.test.ts b/packages/react/test/tanstackrouter.test.ts index 5ae27a3550c9..4922d3c39ca8 100644 --- a/packages/react/test/tanstackrouter.test.ts +++ b/packages/react/test/tanstackrouter.test.ts @@ -1,6 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter'; @@ -84,7 +83,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, { name: '/posts/$postId', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.react.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/posts/$postId', 'url.path.parameter.postId': '999', diff --git a/packages/solid/src/solidrouter.ts b/packages/solid/src/solidrouter.ts index 41f440b66fe0..40f91c2323ed 100644 --- a/packages/solid/src/solidrouter.ts +++ b/packages/solid/src/solidrouter.ts @@ -14,16 +14,11 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; import type { Client, Integration, Span } from '@sentry/core'; -import { - getClient, - hasSpanStreamingEnabled, - NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - filterCollectedUrl, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, filterCollectedUrl } from '@sentry/core'; import type { BeforeLeaveEventArgs, HashRouter, @@ -70,7 +65,7 @@ function handleNavigation(location: string): void { name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : location, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.${framework}.solidrouter`, + [SENTRY_ORIGIN]: `auto.navigation.${framework}.solidrouter`, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }, diff --git a/packages/solid/src/tanstackrouter.ts b/packages/solid/src/tanstackrouter.ts index 29dd7874924a..fd64dce25235 100644 --- a/packages/solid/src/tanstackrouter.ts +++ b/packages/solid/src/tanstackrouter.ts @@ -13,6 +13,7 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; import type { Integration } from '@sentry/core'; @@ -20,7 +21,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, } from '@sentry/core'; import type { AnyRouter } from '@tanstack/solid-router'; @@ -97,7 +97,7 @@ export function tanstackRouterBrowserTracingIntegration( : initialWindowLocation.pathname, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), @@ -156,7 +156,7 @@ export function tanstackRouterBrowserTracingIntegration( name: routeMatch ? routeMatch.routeId : fallbackName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.solid.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), diff --git a/packages/solid/test/solidrouter.test.tsx b/packages/solid/test/solidrouter.test.tsx index 07ae1fd2aaa2..36d36b11b2c5 100644 --- a/packages/solid/test/solidrouter.test.tsx +++ b/packages/solid/test/solidrouter.test.tsx @@ -1,7 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { spanToJSON } from '@sentry/browser'; import type { Span } from '@sentry/core'; -import { createTransport, getCurrentScope, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import type { MemoryHistory } from '@solidjs/router'; import { createMemoryHistory, MemoryRouter, Navigate, Route } from '@solidjs/router'; import { render, waitFor } from '@solidjs/testing-library'; @@ -69,7 +69,7 @@ describe('solidRouterBrowserTracingIntegration', () => { attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -100,7 +100,7 @@ describe('solidRouterBrowserTracingIntegration', () => { attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -145,7 +145,7 @@ describe('solidRouterBrowserTracingIntegration', () => { expect(span.attributes).toMatchObject({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', + [SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', }); for (const [key, value] of Object.entries(expectedParams as Record)) { @@ -181,7 +181,7 @@ describe('solidRouterBrowserTracingIntegration', () => { attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', + [SENTRY_ORIGIN]: 'auto.navigation.solid.solidrouter', }), }), ); diff --git a/packages/solid/test/tanstackrouter.test.ts b/packages/solid/test/tanstackrouter.test.ts index de3a2e9496d7..92908d5b1aa0 100644 --- a/packages/solid/test/tanstackrouter.test.ts +++ b/packages/solid/test/tanstackrouter.test.ts @@ -1,6 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter'; @@ -91,7 +90,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, { name: '/posts/$postId', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.solid.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/posts/$postId', 'url.path.parameter.postId': '999', diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index 62fbc3a144d0..59f7c1b8174a 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { debug, @@ -8,7 +7,7 @@ import { UI_UPDATE_SPAN_NAME_FALLBACK, } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core/browser'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_UPDATE } from '@sentry/conventions/op'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; import { DEBUG_BUILD } from './debug_build'; @@ -65,7 +64,7 @@ function recordInitSpan(componentName: string | undefined, description: string): name: hasSpanStreaming ? componentName || UI_MOUNT_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_MOUNT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte', + [SENTRY_ORIGIN]: 'auto.ui.svelte', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -87,7 +86,7 @@ function recordUpdateSpans(componentName: string | undefined, description: strin name: hasSpanStreaming ? componentName || UI_UPDATE_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_UPDATE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte', + [SENTRY_ORIGIN]: 'auto.ui.svelte', ...(componentName && { [UI_COMPONENT_NAME]: componentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 6e8db5868f85..3f28ec5259b4 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -6,6 +6,7 @@ import { SENTRY_OP, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; import type { Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/core'; @@ -17,7 +18,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; @@ -134,7 +134,7 @@ export function instrumentVueRouter( // This will override the origin, and add params & query attributes activePageLoadSpan.setAttributes({ ...attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + [SENTRY_ORIGIN]: 'auto.pageload.vue', }); hasHandledFirstPageLoad = true; @@ -153,7 +153,7 @@ export function instrumentVueRouter( attributes: { ...attributes, [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: transactionSource, }, }, diff --git a/packages/vue/src/tanstackrouter.ts b/packages/vue/src/tanstackrouter.ts index ed2c659c76f9..f485c6cb3fd3 100644 --- a/packages/vue/src/tanstackrouter.ts +++ b/packages/vue/src/tanstackrouter.ts @@ -13,6 +13,7 @@ import { URL_PATH, URL_PATH_PARAMETER_KEY_BASE, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; import type { Integration } from '@sentry/core'; @@ -20,7 +21,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, } from '@sentry/core'; import type { AnyRouter } from '@tanstack/vue-router'; @@ -103,7 +103,7 @@ export function tanstackRouterBrowserTracingIntegration( : initialWindowLocation.pathname, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), @@ -163,7 +163,7 @@ export function tanstackRouterBrowserTracingIntegration( name: routeMatch ? routeMatch.routeId : fallbackName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: routeMatch ? 'route' : 'url', ...(routeMatch && { [URL_TEMPLATE]: routeMatch.routeId }), ...routeMatchToParamSpanAttributes(routeMatch), diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index a6084cd58ff0..822ef2909a45 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,4 +1,4 @@ -import { getActiveSpan, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/browser'; +import { getActiveSpan, getClient, startInactiveSpan } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { debug, @@ -10,7 +10,7 @@ import { UI_UPDATE_SPAN_NAME_FALLBACK, uniq, } from '@sentry/core'; -import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME } from '@sentry/conventions/attributes'; +import { SENTRY_DESCRIPTION, SENTRY_OP, UI_COMPONENT_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { UI_MOUNT, UI_RENDER, UI_UNMOUNT, UI_UPDATE } from '@sentry/conventions/op'; import { DEFAULT_HOOKS, DEFAULT_ROOT_SPAN_TIMEOUT } from './constants'; import { DEBUG_BUILD } from './debug-build'; @@ -145,7 +145,7 @@ export const createTracingMixins = (options: Partial = {}): Mixi name: hasSpanStreaming ? conventionComponentName || UI_RENDER_SPAN_NAME_FALLBACK : description, attributes: { [SENTRY_OP]: UI_RENDER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', + [SENTRY_ORIGIN]: 'auto.ui.vue', ...(conventionComponentName && { [UI_COMPONENT_NAME]: conventionComponentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -196,7 +196,7 @@ export const createTracingMixins = (options: Partial = {}): Mixi : description, attributes: { [SENTRY_OP]: VUE_OPERATION_TO_SPAN_OP[operation], - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', + [SENTRY_ORIGIN]: 'auto.ui.vue', ...(conventionComponentName && { [UI_COMPONENT_NAME]: conventionComponentName }), ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 94aed649b7e8..bd0972bfcd09 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,8 +1,12 @@ import * as SentryBrowser from '@sentry/browser'; import type { Span, SpanAttributes } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { SENTRY_SEGMENT_NAME_SOURCE, ROUTER_NAVIGATION_ROUTE_ID, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + ROUTER_NAVIGATION_ROUTE_ID, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Route } from '../src/router'; import { instrumentVueRouter } from '../src/router'; @@ -127,7 +131,7 @@ describe('instrumentVueRouter()', () => { name: transactionName, attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: transactionSource, ...getAttributesForRoute(to, transactionSource === 'route' ? transactionName : undefined), }, @@ -179,7 +183,7 @@ describe('instrumentVueRouter()', () => { expect(mockRootSpan.updateName).toHaveBeenCalledWith(transactionName); expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SENTRY_SEGMENT_NAME_SOURCE, transactionSource); expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + [SENTRY_ORIGIN]: 'auto.pageload.vue', ...getAttributesForRoute(to, transactionSource === 'route' ? transactionName : undefined), }); }, @@ -207,7 +211,7 @@ describe('instrumentVueRouter()', () => { name: '/login', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', ...getAttributesForRoute(to, '/login'), }, @@ -238,7 +242,7 @@ describe('instrumentVueRouter()', () => { name: 'login-screen', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', ...getAttributesForRoute(to), }, @@ -298,7 +302,7 @@ describe('instrumentVueRouter()', () => { expect(mockRootSpan.updateName).not.toHaveBeenCalled(); expect(mockRootSpan.setAttribute).not.toHaveBeenCalled(); expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue', + [SENTRY_ORIGIN]: 'auto.pageload.vue', ...getAttributesForRoute(to, '/books/:bookId/chapter/:chapterId'), }); expect(mockRootSpan.name).toEqual('customTxnName'); @@ -471,7 +475,7 @@ describe('instrumentVueRouter()', () => { name: 'Navigation', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', + [SENTRY_ORIGIN]: 'auto.navigation.vue', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', ...getAttributesForRoute(to), }, diff --git a/packages/vue/test/tanstackrouter.test.ts b/packages/vue/test/tanstackrouter.test.ts index 593d4da842ef..54384e81908c 100644 --- a/packages/vue/test/tanstackrouter.test.ts +++ b/packages/vue/test/tanstackrouter.test.ts @@ -1,6 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { AnyRouter } from '@tanstack/vue-router'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter'; @@ -119,7 +118,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, { name: '/test/:id', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path.parameter.id': '123', 'params.id': '123', @@ -292,7 +291,7 @@ describe('tanstackRouterBrowserTracingIntegration', () => { { name: '/test/:id', attributes: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', + [SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', }), }, From f5d1ee7db9664cbb1899575f52cdb43ce9e766de Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 12:20:00 +0200 Subject: [PATCH 12/73] ref(core): Use SENTRY_ORIGIN convention constant in shared tracing (#24983) Uses `SENTRY_ORIGIN` from `@sentry/conventions/attributes` in core, browser-utils, OpenTelemetry tests, and Effect tracing/logging. It has the same `sentry.origin` value; transmitted data and public exports are unchanged. Part of #24961 / [JS-3868](https://linear.app/getsentry/issue/JS-3868). Co-authored-by: GPT-6 --- .../browser-utils/src/performance/entries.ts | 16 ++++++------- .../browser-utils/src/web-vitals/emitSpan.ts | 5 ++-- .../test/performance/browserMetrics.test.ts | 19 +++++---------- packages/core/src/fetch.ts | 4 ++-- .../integrations/http/server-subscription.ts | 4 ++-- .../core/src/integrations/mcp-server/spans.ts | 5 ++-- packages/core/src/integrations/supabase.ts | 7 +++--- packages/core/src/logs/console-integration.ts | 4 ++-- packages/core/src/tracing/sentrySpan.ts | 7 +++--- packages/core/src/trpc.ts | 4 ++-- packages/core/src/utils/spanUtils.ts | 11 ++++++--- packages/core/src/utils/url.ts | 4 ++-- .../lib/tracing/spans/captureSpan.test.ts | 10 ++++---- packages/core/test/lib/tracing/trace.test.ts | 5 ++-- .../core/test/lib/utils/spanUtils.test.ts | 23 +++++++++---------- packages/effect/src/logger.ts | 5 ++-- packages/effect/src/tracer.ts | 5 ++-- packages/effect/test/tracer.test.ts | 11 ++++----- .../test/integration/transactions.test.ts | 19 +++++---------- packages/opentelemetry/test/trace.test.ts | 21 ++++++++--------- 20 files changed, 88 insertions(+), 101 deletions(-) diff --git a/packages/browser-utils/src/performance/entries.ts b/packages/browser-utils/src/performance/entries.ts index f28e60f1cb3e..ce03b8405d7c 100644 --- a/packages/browser-utils/src/performance/entries.ts +++ b/packages/browser-utils/src/performance/entries.ts @@ -6,7 +6,6 @@ import { getActiveSpan, parseUrl, RESOURCE_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, spanToJSON, filterCollectedUrl, @@ -28,6 +27,7 @@ import { URL_DOMAIN, URL_FULL, URL_SCHEME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { BROWSER_CACHE, @@ -123,7 +123,7 @@ export function startTrackingLongTasks(): void { name: UI_LONG_TASK_SPAN_NAME_FALLBACK, op: UI_LONG_TASK, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }, }); } @@ -164,7 +164,7 @@ export function startTrackingLongAnimationFrames(): void { const duration = msToSec(entry.duration); const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }; const initialScript = entry.scripts[0]; @@ -291,7 +291,7 @@ export function _addPaintSpan( name: entry.name, attributes: { [SENTRY_OP]: BROWSER_PAINT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', [BROWSER_PAINT_TYPE]: entry.name, }, }); @@ -370,7 +370,7 @@ function _addPerformanceNavigationTiming( name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[op] : entry.name, attributes: { [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', [URL_FULL]: filterCollectedUrl(entry.name), ...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}), }, @@ -411,7 +411,7 @@ function _addRequest( name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_REQUEST] : entry.name, attributes: { [SENTRY_OP]: BROWSER_REQUEST, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', [URL_FULL]: url, }, }); @@ -420,7 +420,7 @@ function _addRequest( name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_RESPONSE] : entry.name, attributes: { [SENTRY_OP]: BROWSER_RESPONSE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [SENTRY_ORIGIN]: 'auto.ui.browser.metrics', [URL_FULL]: url, }, }); @@ -453,7 +453,7 @@ export function _addResourceSpans( } const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', }; const parsedUrl = parseUrl(resourceUrl); diff --git a/packages/browser-utils/src/web-vitals/emitSpan.ts b/packages/browser-utils/src/web-vitals/emitSpan.ts index ed3602f35e6f..5e25931f4bf8 100644 --- a/packages/browser-utils/src/web-vitals/emitSpan.ts +++ b/packages/browser-utils/src/web-vitals/emitSpan.ts @@ -1,5 +1,5 @@ import type { Integration, Span, SpanAttributes } from '@sentry/core'; -import { getClient, getCurrentScope, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; +import { getClient, getCurrentScope, getRootSpan, spanToJSON } from '@sentry/core'; import { startInactiveSpan } from '@sentry/core/browser'; import { SENTRY_EXCLUSIVE_TIME, @@ -10,6 +10,7 @@ import { SENTRY_TRANSACTION, USER_AGENT_ORIGINAL, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { WINDOW } from '../types'; import type { MetricNavigationType } from '../instrumentation/performanceObserver'; @@ -85,7 +86,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { const segmentName = segmentSpan ? spanToJSON(segmentSpan).name : getCurrentScope().getScopeData().transactionName; const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [SENTRY_ORIGIN]: origin, [SENTRY_OP]: op, [SENTRY_EXCLUSIVE_TIME]: 0, [`browser.web_vital.${metricName}.value`]: value, diff --git a/packages/browser-utils/test/performance/browserMetrics.test.ts b/packages/browser-utils/test/performance/browserMetrics.test.ts index bb87c4d64750..f9dcccabb0b4 100644 --- a/packages/browser-utils/test/performance/browserMetrics.test.ts +++ b/packages/browser-utils/test/performance/browserMetrics.test.ts @@ -1,13 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - getClient, - getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SentrySpan, - setCurrentClient, - spanToJSON, -} from '@sentry/core'; +import { getClient, getMainCarrier, SentrySpan, setCurrentClient, spanToJSON } from '@sentry/core'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { _addNavigationSpans, @@ -247,7 +240,7 @@ describe('_addResourceSpans', () => { end_timestamp: timeOrigin + startTime + duration, attributes: { [SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.response.body.decoded_size']: entry.decodedBodySize, ['http.response.body.size']: entry.encodedBodySize, ['http.response.size']: entry.transferSize, @@ -430,7 +423,7 @@ describe('_addResourceSpans', () => { expect.objectContaining({ attributes: expect.objectContaining({ [SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.response.body.decoded_size']: entry.decodedBodySize, ['http.response.body.size']: entry.encodedBodySize, ['http.response.size']: entry.transferSize, @@ -469,7 +462,7 @@ describe('_addResourceSpans', () => { expect.objectContaining({ attributes: expect.objectContaining({ [SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', 'url.domain': 'example.com', 'http.request.same_origin': true, @@ -521,7 +514,7 @@ describe('_addResourceSpans', () => { expect.objectContaining({ attributes: { [SENTRY_OP]: 'resource.css', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', 'url.domain': 'example.com', 'http.request.same_origin': true, diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index aaf83ef25d82..76ef3892bea1 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -9,11 +9,11 @@ import { URL_FRAGMENT, URL_FULL, URL_QUERY, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import type { Client } from './client'; import { getClient } from './currentScopes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; import { setHttpStatus, SPAN_STATUS_ERROR, spanIsIgnored } from './tracing'; import { startInactiveSpan } from './tracing/trace'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; @@ -385,7 +385,7 @@ function getFetchSpanAttributes( type: 'fetch', // oxlint-disable-next-line typescript/no-deprecated [HTTP_REQUEST_METHOD]: method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + [SENTRY_ORIGIN]: spanOrigin, [SENTRY_OP]: HTTP_CLIENT, [URL_DOMAIN]: domain, }; diff --git a/packages/core/src/integrations/http/server-subscription.ts b/packages/core/src/integrations/http/server-subscription.ts index 3ef8edbf0a49..8427c699e031 100644 --- a/packages/core/src/integrations/http/server-subscription.ts +++ b/packages/core/src/integrations/http/server-subscription.ts @@ -39,7 +39,6 @@ import { continueTrace, startSpanManual } from '../../tracing/trace'; import { getSpanStatusFromHttpCode, SPAN_STATUS_ERROR } from '../../tracing'; import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { HTTP_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { safeMathRandom } from '../../utils/randomSafeContext'; import type { SpanStatus } from '../../types/spanStatus'; import { @@ -66,6 +65,7 @@ import { URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import { filterCollectedUrl, filterCollectedUrlQuery } from '../../utils/data-collection/filterCollectedUrl'; @@ -326,7 +326,7 @@ function buildServerSpanWrap( attributes: { // Sentry-specific attributes [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.server', + [SENTRY_ORIGIN]: 'auto.http.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_KIND]: 'server', // Network attributes diff --git a/packages/core/src/integrations/mcp-server/spans.ts b/packages/core/src/integrations/mcp-server/spans.ts index 9159ce497122..b380da55e5a8 100644 --- a/packages/core/src/integrations/mcp-server/spans.ts +++ b/packages/core/src/integrations/mcp-server/spans.ts @@ -6,13 +6,12 @@ */ import { getClient } from '../../currentScopes'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MCP_NOTIFICATION_CLIENT_TO_SERVER, MCP_NOTIFICATION_SERVER_TO_CLIENT, MCP_SERVER, } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled'; import { MCP_NOTIFICATION_SPAN_NAME_FALLBACK, MCP_SERVER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames'; import { startSpan } from '../../tracing/trace'; @@ -72,7 +71,7 @@ function buildSentryAttributes(type: McpSpanConfig['type']): Record = {}) => { diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 7d149353da1a..f0285f63eb27 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -6,8 +6,9 @@ import { SENTRY_EXCLUSIVE_TIME, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '../semanticAttributes'; import type { Client } from '../client'; import type { TransactionEvent } from '../types/event'; import type { SpanLink } from '../types/link'; @@ -103,7 +104,7 @@ export class SentrySpan implements Span { this._attributes = {}; this.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SENTRY_OP]: spanContext.op, ...spanContext.attributes, }); @@ -278,7 +279,7 @@ export class SentrySpan implements Span { status: getStatusMessage(this._status), timestamp: this._endTime, trace_id: this._traceId, - origin: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + origin: this._attributes[SENTRY_ORIGIN] as SpanOrigin | undefined, profile_id: this._attributes[SENTRY_PROFILE_ID] as string | undefined, exclusive_time: this._attributes[SENTRY_EXCLUSIVE_TIME] as number | undefined, measurements: timedEventsToMeasurements(this._events), diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 55e428f60458..d84f6f16f17b 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -5,11 +5,11 @@ import { SENTRY_SEGMENT_NAME_SOURCE, TRPC_PROCEDURE_PATH, TRPC_PROCEDURE_TYPE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; import { getClient, withIsolationScope } from './currentScopes'; import { captureException } from './exports'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; import { startSpanManual } from './tracing/trace'; import { normalize } from './utils/normalize'; import { setNormalizationDepthOverrideHint } from './utils/normalizationHints'; @@ -97,7 +97,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { attributes: { [SENTRY_OP]: RPC, [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.rpc.trpc', + [SENTRY_ORIGIN]: 'auto.rpc.trpc', [RPC_SYSTEM_NAME]: 'trpc', [RPC_METHOD]: String(path), [TRPC_PROCEDURE_PATH]: String(path), diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index a643caeccaba..e1615b9bf5c4 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -5,8 +5,13 @@ import { serializeAttributes } from '../attributes'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; import type { Scope } from '../scope'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_STATUS_MESSAGE, SENTRY_OP } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_STATUS_MESSAGE, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { isStatusErrorMessageValid, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; import { getCapturedScopesOnSpan } from '../tracing/utils'; @@ -187,7 +192,7 @@ export function spanToStaticSpanJSON(span: Span): SpanJSON { timestamp: spanTimeInputToSeconds(endTime) || undefined, status: getStatusMessage(status), op: attributes[SENTRY_OP], - origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + origin: attributes[SENTRY_ORIGIN] as SpanOrigin | undefined, links: convertSpanLinksForEnvelope(links), }; } diff --git a/packages/core/src/utils/url.ts b/packages/core/src/utils/url.ts index d7f45141a342..3a0a5fca186c 100644 --- a/packages/core/src/utils/url.ts +++ b/packages/core/src/utils/url.ts @@ -11,8 +11,8 @@ import { SENTRY_SEGMENT_NAME_SOURCE, URL_SCHEME, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { Client } from '../client'; import type { SpanAttributes } from '../types/span'; import type { TransactionSource } from '../types/transaction'; @@ -210,7 +210,7 @@ export function getHttpSpanDetailsFromUrlObject( client?: Client, ): [name: string, attributes: SpanAttributes] { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + [SENTRY_ORIGIN]: spanOrigin, }; let nameSource: TransactionSource = 'url'; diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 5f72e42a622b..968e9a68be1b 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -3,7 +3,6 @@ import type { Contexts, Span, StreamedSpanJSON } from '../../../../src'; import { captureSpan, debug, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanStreamingIntegration, startInactiveSpan, @@ -33,6 +32,7 @@ import { SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; describe('captureSpan', () => { @@ -81,7 +81,7 @@ describe('captureSpan', () => { type: 'string', value: 'http.client', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -177,7 +177,7 @@ describe('captureSpan', () => { type: 'string', value: 'http.client', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -273,7 +273,7 @@ describe('captureSpan', () => { type: 'string', value: 'http.client', }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { + [SENTRY_ORIGIN]: { type: 'string', value: 'manual', }, @@ -351,7 +351,7 @@ describe('captureSpan', () => { attributes: { [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, [SENTRY_OP]: { type: 'string', value: 'http.client' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SENTRY_SEGMENT_NAME]: { value: 'my-span', type: 'string' }, [SENTRY_SEGMENT_ID]: { value: span.spanContext().spanId, type: 'string' }, diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 9f54b7e693c9..5d7ab473b1b8 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -6,7 +6,6 @@ import { getMainCarrier, getTraceData, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setAsyncContextStrategy, setCurrentClient, spanToJSON, @@ -32,7 +31,7 @@ import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '. import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; import { SUPPRESS_TRACING_KEY } from '../../../src/tracing/constants'; import { resetGlobals } from '../../testutils'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; const enum Type { Sync = 'sync', @@ -179,7 +178,7 @@ describe('startSpan', () => { await startSpan( { name: 'GET users/[id]', - attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser' }, + attributes: { [SENTRY_ORIGIN]: 'auto.http.browser' }, }, () => { return callback(); diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index c11a2fa3317d..032a09ce1667 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,10 +1,9 @@ -import { SENTRY_LINK_TYPE, SENTRY_STATUS_MESSAGE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_LINK_TYPE, SENTRY_STATUS_MESSAGE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, test } from 'vitest'; import { convertSpanLinksForEnvelope, getCurrentScope, Scope, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, SentrySpan, setCurrentClient, @@ -343,7 +342,7 @@ describe('spanToStaticSpanJSON', () => { traceId: 'abcd', startTimestamp: 123, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, }); span.setStatus({ code: SPAN_STATUS_OK }); @@ -400,7 +399,7 @@ describe('spanToStaticSpanJSON', () => { attr1: 'value1', attr2: 2, [SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, status: { code: SPAN_STATUS_ERROR, message: 'unknown_error' }, }); @@ -417,7 +416,7 @@ describe('spanToStaticSpanJSON', () => { attr1: 'value1', attr2: 2, [SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, status: 'unknown_error', }); @@ -437,7 +436,7 @@ describe('spanToStaticSpanJSON', () => { status: 'ok', is_segment: true, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', }, }); }); @@ -451,7 +450,7 @@ describe('spanToStaticSpanJSON', () => { traceId: 'abcd', startTimestamp: 123, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', attr1: 'value1', attr2: 2, attr3: true, @@ -488,7 +487,7 @@ describe('spanToStaticSpanJSON', () => { attr3: true, attr4: [1, 2, 3], [SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, links: [ { @@ -586,7 +585,7 @@ describe('spanToStaticSpanJSON', () => { attr1: 'value1', attr2: 2, [SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, links: [ { @@ -616,7 +615,7 @@ describe('spanToStaticSpanJSON', () => { attr1: 'value1', attr2: 2, [SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', [SENTRY_STATUS_MESSAGE]: 'unknown_error', }, links: [ @@ -683,7 +682,7 @@ describe('spanToStaticSpanJSON', () => { attr3: true, attr4: [1, 2, 3], [SENTRY_OP]: 'test op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + [SENTRY_ORIGIN]: 'auto', }, links: [ { @@ -712,7 +711,7 @@ describe('spanToStaticSpanJSON', () => { attr3: { type: 'boolean', value: true }, attr4: { type: 'array', value: [1, 2, 3] }, [SENTRY_OP]: { type: 'string', value: 'test op' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, }, links: [ { diff --git a/packages/effect/src/logger.ts b/packages/effect/src/logger.ts index f0a55b3bf20e..a5fc7fe6923d 100644 --- a/packages/effect/src/logger.ts +++ b/packages/effect/src/logger.ts @@ -1,9 +1,10 @@ -import { isObjectLike, logger as sentryLogger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { isObjectLike, logger as sentryLogger } from '@sentry/core'; import * as Logger from 'effect/Logger'; import type * as LogLevel from 'effect/LogLevel'; const LOG_ATTRIBUTES = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.log.effect', + [SENTRY_ORIGIN]: 'auto.log.effect', }; function getLogLevelTag(logLevel: LogLevel.LogLevel): LogLevel.LogLevel | 'Warning' { diff --git a/packages/effect/src/tracer.ts b/packages/effect/src/tracer.ts index 2e72e3c61fc0..f25ce111b180 100644 --- a/packages/effect/src/tracer.ts +++ b/packages/effect/src/tracer.ts @@ -1,5 +1,5 @@ /* oxlint-disable max-lines */ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_CLIENT, HTTP_SERVER } from '@sentry/conventions/op'; import type { Span, StartSpanOptions } from '@sentry/core'; import { @@ -9,7 +9,6 @@ import { getCurrentScope, getDefaultCurrentScope, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startNewTrace, withActiveSpan, withScope, @@ -390,7 +389,7 @@ function createSentrySpan( // Setting these to `undefined` would strip the core defaults instead of leaving them in place. attributes: { ...(op && { [SENTRY_OP]: op }), - ...(origin && { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin }), + ...(origin && { [SENTRY_ORIGIN]: origin }), }, }, parent, diff --git a/packages/effect/test/tracer.test.ts b/packages/effect/test/tracer.test.ts index 66ccf6cb9e0a..c1f4a767da93 100644 --- a/packages/effect/test/tracer.test.ts +++ b/packages/effect/test/tracer.test.ts @@ -1,8 +1,7 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { describe, expect, it } from '@effect/vitest'; import * as sentryCore from '@sentry/core'; import * as sentryCoreBrowser from '@sentry/core/browser'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { ServerRuntimeClient } from '@sentry/core/server'; import { Effect } from 'effect'; import * as Tracer from 'effect/Tracer'; @@ -202,9 +201,9 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa Effect.gen(function* () { const attributes = yield* attributesFor('my-operation'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + expect(attributes?.[SENTRY_ORIGIN]).toBeUndefined(); expect(attributes?.[SENTRY_OP]).toBeUndefined(); - expect(attributes).not.toHaveProperty(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN); + expect(attributes).not.toHaveProperty(SENTRY_ORIGIN); expect(attributes).not.toHaveProperty(SENTRY_OP); }), ); @@ -213,7 +212,7 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa Effect.gen(function* () { const attributes = yield* attributesFor('http.server GET /api/users'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); + expect(attributes?.[SENTRY_ORIGIN]).toBe('auto.http.effect'); expect(attributes?.[SENTRY_OP]).toBe('http.server'); }), ); @@ -222,7 +221,7 @@ describe.each(VARIANTS)('SentryEffectTracer ($variant)', ({ variant, tracer, spa Effect.gen(function* () { const attributes = yield* attributesFor('http.client GET https://api.example.com'); - expect(attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.effect'); + expect(attributes?.[SENTRY_ORIGIN]).toBe('auto.http.effect'); expect(attributes?.[SENTRY_OP]).toBe('http.client'); }), ); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 4ff73f163f0c..21f24a0f8555 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -1,16 +1,9 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { SpanContext } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; import { TraceState } from '../../src/utils/TraceState'; import type { Event, TransactionEvent } from '@sentry/core'; -import { - addBreadcrumb, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setTag, - startInactiveSpan, - startSpan, - withIsolationScope, -} from '@sentry/core'; +import { addBreadcrumb, setTag, startInactiveSpan, startSpan, withIsolationScope } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { SENTRY_TRACE_STATE_DSC } from '../../src/constants'; import { makeTraceState } from '../../src/utils/makeTraceState'; @@ -44,7 +37,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, span => { @@ -168,7 +161,7 @@ describe('Integration | Transactions', () => { op: 'test op', name: 'test name', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, @@ -324,7 +317,7 @@ describe('Integration | Transactions', () => { op: 'test op', name: 'test name', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, @@ -438,7 +431,7 @@ describe('Integration | Transactions', () => { op: 'test op', name: 'test name', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 957f034b801d..448692f6a505 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,7 +1,7 @@ /* eslint-disable typescript/no-deprecated */ import type { Span, TimeInput } from '@opentelemetry/api'; import { context, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_KIND, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_KIND, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Event, Scope } from '@sentry/core'; import { getCapturedScopesOnSpan, @@ -10,7 +10,6 @@ import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToJSON, startInactiveSpan, @@ -210,7 +209,7 @@ describe('trace', () => { span => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', }); @@ -223,14 +222,14 @@ describe('trace', () => { op: 'my-op', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_ORIGIN]: 'auto.test.origin', }, }, span => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_ORIGIN]: 'auto.test.origin', [SENTRY_OP]: 'my-op', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, }); @@ -256,7 +255,7 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', test1: 'test 1', @@ -607,7 +606,7 @@ describe('trace', () => { expect(span).toBeDefined(); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', }); @@ -617,7 +616,7 @@ describe('trace', () => { op: 'my-op', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_ORIGIN]: 'auto.test.origin', }, }); @@ -625,7 +624,7 @@ describe('trace', () => { expect(getSpanAttributes(span2)).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test.origin', + [SENTRY_ORIGIN]: 'auto.test.origin', [SENTRY_OP]: 'my-op', }); }); @@ -647,7 +646,7 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', test1: 'test 1', @@ -980,7 +979,7 @@ describe('trace', () => { expect(getSpanName(span)).toEqual('outer'); expect(getSpanStartTime(span)).toEqual(date); expect(getSpanAttributes(span)).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', test1: 'test 1', From 2998151d72203d1534987968e96638e6159b6d3c Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:26:37 +0200 Subject: [PATCH 13/73] test(e2e): Add eve test for Jev evaluate calls (#25034) ## What Adds an e2e test to the eve app. The test makes an agent tool call a TypeSafe Jev evaluation. ## Why Make sure a Jev evaluation in an eve tool records a `gen_ai.evaluate` span under the tool span. --------- Co-authored-by: Claude Opus 5.5 --- .../node-eve/agent/instructions.md | 1 + .../node-eve/agent/tools/classify_ticket.ts | 43 +++++++++++++++++++ .../node-eve/tests/eve.test.ts | 31 +++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts diff --git a/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md b/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md index e67cae081e40..ecdf9ca459ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md +++ b/dev-packages/e2e-tests/test-applications/node-eve/agent/instructions.md @@ -4,5 +4,6 @@ You are a concise assistant used by an automated end-to-end test. for that place and answer in one short sentence using its result. - When the user asks to count items, call the `count_items` tool with the item names. - When the user asks you to trigger a failure, call the `fail_now` tool. +- When the user asks to classify a ticket, call the `classify_ticket` tool with the ticket text. Do not ask follow-up questions. diff --git a/dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts b/dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts new file mode 100644 index 000000000000..9dd65a11fd90 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-eve/agent/tools/classify_ticket.ts @@ -0,0 +1,43 @@ +import { experimental_evaluate } from 'ai'; +import { Experimental_EvaluationMockModelV4 } from 'ai/test'; +import { defineTool } from 'eve/tools'; +import { z } from 'zod'; + +// Runs a TypeSafe Jev evaluation so the e2e test can assert its `gen_ai.evaluate` span. The model is +// mocked because the e2e environment has no TypeSafe credentials. +const jev = new Experimental_EvaluationMockModelV4({ + provider: 'gateway', + modelId: 'typesafe-ai/jev', + doEvaluate: async () => ({ + answers: { + wantsRefund: { type: 'boolean', probability: 0.99 }, + department: { + type: 'choice', + choice: 'billing', + probabilities: { billing: 0.64, technical: 0.36 }, + }, + }, + usage: { inputTokens: 275, outputTokens: 20 }, + warnings: [], + }), +}); + +export default defineTool({ + description: 'Classify a support ticket. Call this when asked to classify a ticket.', + inputSchema: z.object({ ticket: z.string().min(1) }), + async execute({ ticket }) { + const { answers } = await experimental_evaluate({ + model: jev, + state: ticket, + questions: { + wantsRefund: { type: 'boolean', instructions: 'Is a refund requested?' }, + department: { + type: 'choice', + instructions: 'Which team should handle this?', + criteria: { billing: 'Charges and refunds', technical: 'Bugs and outages' }, + }, + }, + }); + return answers; + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts b/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts index 9099824bd7be..04ad1e5151f4 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts @@ -134,3 +134,34 @@ test('captures errors thrown inside an eve tool', async ({ baseURL }) => { transaction: expect.stringMatching(EVE_AGENT_PATH), }); }); + +test('captures a gen_ai.evaluate span for a Jev call inside an eve tool', async ({ baseURL }) => { + const traceSpansPromise = collectStreamedSpans(APP, spansOfTrace => + ['gen_ai.execute_tool', 'gen_ai.evaluate'].every(op => spansOfTrace.some(span => getSpanOp(span) === op)), + ); + + const sessionId = await runAgentTurn( + baseURL!, + 'Classify this ticket: I cannot log in, and I also want a refund for last month.', + ); + + const traceSpans = await traceSpansPromise; + + const executeTool = traceSpans.find(span => getSpanOp(span) === 'gen_ai.execute_tool'); + const evaluate = traceSpans.find(span => getSpanOp(span) === 'gen_ai.evaluate'); + + expect(executeTool?.attributes?.['gen_ai.tool.name']?.value).toBe('classify_ticket'); + + expect(evaluate?.name).toBe('evaluate typesafe-ai/jev'); + expect(evaluate?.status).toBe('ok'); + expect(evaluate?.attributes?.['sentry.origin']?.value).toBe('auto.vercelai.channel'); + expect(evaluate?.attributes?.['gen_ai.operation.name']?.value).toBe('evaluate'); + expect(evaluate?.attributes?.['gen_ai.request.model']?.value).toBe('typesafe-ai/jev'); + expect(evaluate?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe(275); + expect(evaluate?.attributes?.['gen_ai.usage.output_tokens']?.value).toBe(20); + expect(evaluate?.attributes?.['gen_ai.input.messages']?.value).toContain('refund for last month'); + expect(evaluate?.attributes?.['gen_ai.conversation.id']?.value).toBe(sessionId); + + expect(evaluate?.trace_id).toBe(executeTool?.trace_id); + expect(evaluate?.parent_span_id).toBe(executeTool?.span_id); +}); From f94c52e0f3735fe774cd7294d6200d0b3bfa4937 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 12:29:39 +0200 Subject: [PATCH 14/73] ref(meta-frameworks): Use SENTRY_ORIGIN convention constant (#25038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses `SENTRY_ORIGIN` from `@sentry/conventions/attributes` in full-stack framework integrations and their tests. It has the same `sentry.origin` value; transmitted data and public exports are unchanged. Nuxt’s private database-origin constant is renamed to avoid an import collision; its value remains `auto.db.nuxt`. Part of #24961 / [JS-3868](https://linear.app/getsentry/issue/JS-3868). Co-authored-by: GPT-6 --- .../nuxt-4-static/tests/cache.test.ts | 17 +++------ .../tests/storage-aliases.test.ts | 17 ++++----- .../nuxt-4-static/tests/storage.test.ts | 23 +++++------- .../tests/tracing.server.test.ts | 5 +-- .../tests/performance.server.test.ts | 7 ++-- .../src/client/browserTracingIntegration.ts | 11 ++---- packages/astro/src/server/middleware.ts | 6 +-- .../appRouterRoutingInstrumentation.ts | 10 ++--- .../pagesRouterNavigationInstrumentation.ts | 11 ++---- .../pagesRouterRoutingInstrumentation.ts | 12 ++---- .../utils/dropMiddlewareTunnelRequests.ts | 6 +-- .../common/withServerActionInstrumentation.ts | 4 +- packages/nextjs/src/edge/index.ts | 4 +- .../nextjs/src/server/handleOnSpanStart.ts | 6 +-- .../src/runtime/hooks/captureStorageEvents.ts | 5 +-- .../src/runtime/hooks/captureTracingEvents.ts | 9 ++--- .../runtime/hooks/wrapMiddlewareHandler.ts | 7 ++-- .../src/runtime/utils/instrumentDatabase.ts | 9 ++--- .../src/runtime/utils/instrumentStorage.ts | 7 ++-- .../src/client/createClientInstrumentation.ts | 18 ++++----- .../react-router/src/client/hydratedRouter.ts | 13 +++++-- .../src/server/createServerInstrumentation.ts | 14 +++---- .../src/server/wrapSentryHandleRequest.ts | 12 ++---- .../server/wrapSentryHandleRequest.test.ts | 10 ++--- packages/remix/src/client/performance.tsx | 7 ++-- packages/remix/src/server/instrumentServer.ts | 8 ++-- .../server/integrations/tracing-channel.ts | 8 ++-- .../v3/client/browserTracingIntegration.ts | 12 ++---- .../server/withServerActionInstrumentation.ts | 5 ++- .../test/client/solidrouter.test.tsx | 12 +++--- .../withServerActionInstrumentation.test.ts | 17 ++++----- packages/sveltekit/src/client/load.ts | 4 +- .../src/client/svelte4BrowserTracing.ts | 9 ++--- .../src/client/svelte5BrowserTracing.ts | 9 ++--- .../sveltekit/src/server-common/handle.ts | 6 +-- .../integrations/svelteKitSpans.ts | 14 +++---- packages/sveltekit/src/server-common/load.ts | 13 ++----- .../src/server-common/serverRoute.ts | 4 +- .../client/browserTracingIntegration.test.ts | 13 +++---- packages/sveltekit/test/client/load.test.ts | 11 +++--- .../test/client/svelte5BrowserTracing.test.ts | 7 ++-- .../integrations/svelteKitSpans.test.ts | 37 +++++++++---------- .../sveltekit/test/server-common/load.test.ts | 15 ++++---- .../test/server-common/serverRoute.test.ts | 12 +++--- .../tanstackstart-react/src/server/utils.ts | 5 +-- .../src/server/wrapFetchWithSentry.ts | 12 ++++-- 46 files changed, 217 insertions(+), 266 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts index 677322ccae16..93eace9154e4 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/cache.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Cache Instrumentation', () => { const SEMANTIC_ATTRIBUTE_CACHE_KEY = 'cache.key'; @@ -25,9 +24,7 @@ test.describe('Cache Instrumentation', () => { }; // Test that we have cache operations from cachedFunction and cachedEventHandler - const allCacheSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allCacheSpans = transaction.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); expect(allCacheSpans?.length).toBeGreaterThan(0); // Test getItem spans for cachedFunction - should have both cache miss and cache hit @@ -44,7 +41,7 @@ test.describe('Cache Instrumentation', () => { if (cacheMissSpan) { expect(cacheMissSpan.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_HIT]: false, 'db.operation.name': 'getItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), @@ -61,7 +58,7 @@ test.describe('Cache Instrumentation', () => { if (cacheHitSpan) { expect(cacheHitSpan.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), @@ -80,7 +77,7 @@ test.describe('Cache Instrumentation', () => { if (cacheSetSpan) { expect(cacheSetSpan.data).toMatchObject({ [SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'setItem', 'db.collection.name': expect.stringMatching(/^(cache)?$/), }); @@ -126,9 +123,7 @@ test.describe('Cache Instrumentation', () => { const transaction1 = await transactionPromise; // Get all cache-related spans - const allCacheSpans = transaction1.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allCacheSpans = transaction1.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); // We should have cache operations expect(allCacheSpans?.length).toBeGreaterThan(0); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts index 2d56c4cd5449..e6ef2dfc364e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage-aliases.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation - Aliases', () => { const prefixKey = (key: string) => `test-storage:${key}`; @@ -30,7 +29,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(setSpan).toBeDefined(); expect(setSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), 'db.operation.name': 'setItem', 'db.collection.name': 'test-storage', @@ -45,7 +44,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(getSpan).toBeDefined(); expect(getSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', @@ -61,7 +60,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(hasSpan).toBeDefined(); expect(hasSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:user'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'hasItem', @@ -77,7 +76,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(delSpan).toBeDefined(); expect(delSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp1'), 'db.operation.name': 'removeItem', 'db.collection.name': 'test-storage', @@ -89,7 +88,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(removeSpan).toBeDefined(); expect(removeSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('alias:temp2'), 'db.operation.name': 'removeItem', 'db.collection.name': 'test-storage', @@ -98,9 +97,7 @@ test.describe('Storage Instrumentation - Aliases', () => { expect(removeSpan?.description).toBe(prefixKey('alias:temp2')); // Verify all spans have OK status - const allStorageSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allStorageSpans = transaction.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); expect(allStorageSpans?.length).toBeGreaterThan(0); allStorageSpans?.forEach(span => { expect(span.status).toBe('ok'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts index e16a0213c0a2..5dd3e7daa680 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/storage.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test.describe('Storage Instrumentation', () => { const prefixKey = (key: string) => `test-storage:${key}`; @@ -30,7 +29,7 @@ test.describe('Storage Instrumentation', () => { expect(setItemSpan).toBeDefined(); expect(setItemSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), 'db.operation.name': 'setItem', 'db.collection.name': 'test-storage', @@ -47,7 +46,7 @@ test.describe('Storage Instrumentation', () => { expect(setItemRawSpan).toBeDefined(); expect(setItemRawSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.put', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), 'db.operation.name': 'setItemRaw', 'db.collection.name': 'test-storage', @@ -61,7 +60,7 @@ test.describe('Storage Instrumentation', () => { expect(hasItemSpan).toBeDefined(); expect(hasItemSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'hasItem', @@ -76,7 +75,7 @@ test.describe('Storage Instrumentation', () => { expect(getItemSpan).toBeDefined(); expect(getItemSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('user:123'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItem', @@ -94,7 +93,7 @@ test.describe('Storage Instrumentation', () => { expect(getItemRawSpan).toBeDefined(); expect(getItemRawSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('raw:data'), [SEMANTIC_ATTRIBUTE_CACHE_HIT]: true, 'db.operation.name': 'getItemRaw', @@ -107,7 +106,7 @@ test.describe('Storage Instrumentation', () => { expect(getKeysSpans.length).toBeGreaterThanOrEqual(1); expect(getKeysSpans[0]?.data).toMatchObject({ [SENTRY_OP]: 'cache.get', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'getKeys', 'db.collection.name': 'test-storage', 'db.system.name': 'memory', @@ -122,7 +121,7 @@ test.describe('Storage Instrumentation', () => { expect(removeItemSpan).toBeDefined(); expect(removeItemSpan?.data).toMatchObject({ [SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [SEMANTIC_ATTRIBUTE_CACHE_KEY]: prefixKey('batch:1'), 'db.operation.name': 'removeItem', 'db.collection.name': 'test-storage', @@ -134,16 +133,14 @@ test.describe('Storage Instrumentation', () => { expect(clearSpans.length).toBeGreaterThanOrEqual(1); expect(clearSpans[0]?.data).toMatchObject({ [SENTRY_OP]: 'cache.remove', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', 'db.operation.name': 'clear', 'db.collection.name': 'test-storage', 'db.system.name': 'memory', }); // Verify all spans have OK status - const allStorageSpans = transaction.spans?.filter( - span => span.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.cache.nuxt', - ); + const allStorageSpans = transaction.spans?.filter(span => span.data?.[SENTRY_ORIGIN] === 'auto.cache.nuxt'); expect(allStorageSpans?.length).toBeGreaterThan(0); allStorageSpans?.forEach(span => { expect(span.status).toBe('ok'); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts index 1a2dc1744613..58f65a87ec26 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/tests/tracing.server.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/nuxt'; test('sends a server action transaction on pageload', async ({ page }) => { const transactionPromise = waitForTransaction('nuxt-4-static', transactionEvent => { @@ -16,7 +15,7 @@ test('sends a server action transaction on pageload', async ({ page }) => { expect.objectContaining({ data: expect.objectContaining({ [SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server', + [SENTRY_ORIGIN]: 'auto.http.http_server', }), }), ); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts index c593a0fd396d..814908bf80e4 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/tests/performance.server.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/solidstart'; test('sends a server action transaction on pageload', async ({ page }) => { const transactionPromise = waitForTransaction('solidstart-static', transactionEvent => { @@ -18,7 +17,7 @@ test('sends a server action transaction on pageload', async ({ page }) => { description: 'getPrefecture', data: { [SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SENTRY_ORIGIN]: 'auto.function.solidstart', 'code.function.name': 'getPrefecture', }, }), @@ -43,7 +42,7 @@ test('sends a server action transaction on client navigation', async ({ page }) description: 'getPrefecture', data: { [SENTRY_OP]: 'function', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SENTRY_ORIGIN]: 'auto.function.solidstart', 'code.function.name': 'getPrefecture', }, }), diff --git a/packages/astro/src/client/browserTracingIntegration.ts b/packages/astro/src/client/browserTracingIntegration.ts index 86e8a009e0cd..80545c379b2e 100644 --- a/packages/astro/src/client/browserTracingIntegration.ts +++ b/packages/astro/src/client/browserTracingIntegration.ts @@ -4,14 +4,9 @@ import { WINDOW, } from '@sentry/browser'; import type { Client, Integration, TransactionSource } from '@sentry/core'; -import { - debug, - hasSpanStreamingEnabled, - PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { debug, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; /** * Returns the value of a meta-tag @@ -44,7 +39,7 @@ export function browserTracingIntegration( name, attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: source, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.astro', + [SENTRY_ORIGIN]: 'auto.pageload.astro', ...(source === 'route' && { [URL_TEMPLATE]: name }), }, }); diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 4116085ffb41..2e88b7fc160a 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -8,6 +8,7 @@ import { URL_FULL, URL_PATH, URL_QUERY, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; @@ -34,7 +35,6 @@ import { getCurrentScope, getTraceMetaTags, httpHeadersToSpanAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, startSpan, winterCGHeadersToDict, @@ -148,7 +148,7 @@ async function enhanceHttpServerSpan(ctx: APIContext, next: MiddlewareNext, root rootSpan.setAttributes({ // This is here for backwards compatibility, we used to set this here before method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', + [SENTRY_ORIGIN]: 'auto.http.astro', }); if (parametrizedRoute) { @@ -220,7 +220,7 @@ async function instrumentRequestStartHttpServerSpan( const attributes: SpanAttributes = { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.astro', + [SENTRY_ORIGIN]: 'auto.http.astro', [SENTRY_SEGMENT_NAME_SOURCE]: source, [HTTP_REQUEST_METHOD]: method, // This is here for backwards compatibility, we used to set this here before diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 1a7bb8a113de..26133c1d9747 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -4,7 +4,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, filterCollectedUrl, timestampInSeconds, } from '@sentry/core'; @@ -23,6 +22,7 @@ import { URL_FULL, URL_PATH, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; @@ -104,7 +104,7 @@ export function appRouterInstrumentPageLoad(client: Client): void { // pageload should always start at timeOrigin (and needs to be in s, not ms) attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.pageload.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), }, @@ -179,7 +179,7 @@ export function appRouterInstrumentNavigation(client: Client): void { name: spanName, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': `router.${navigationType}`, ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), @@ -216,7 +216,7 @@ export function appRouterInstrumentNavigation(client: Client): void { name: spanName, startTime: traversal?.startTime, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': traversal?.navigationType ?? 'browser.popstate', ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), @@ -313,7 +313,7 @@ function patchRouter(client: Client, router: NextRouter, currentNavigationSpanRe (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : transactionName), attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url', 'navigation.type': `router.${routerFunctionName}`, ...(parameterizedPathname && { [URL_TEMPLATE]: parameterizedPathname }), diff --git a/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts index 44d842b6193a..ddd14281bfea 100644 --- a/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterNavigationInstrumentation.ts @@ -1,13 +1,8 @@ import type { Client, TransactionSource } from '@sentry/core'; -import { - hasSpanStreamingEnabled, - NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - stripUrlQueryAndFragment, -} from '@sentry/core'; +import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, stripUrlQueryAndFragment } from '@sentry/core'; import { getAbsoluteUrl, startBrowserTracingNavigationSpan } from '@sentry/react'; import RouterImport from 'next/router'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; import { getNextRouteFromPathname } from './pagesRouterRoutingInstrumentation'; @@ -49,7 +44,7 @@ export function pagesRouterInstrumentNavigation(client: Client): void { name: spanSource === 'route' || !hasSpanStreamingEnabled(client) ? newLocation : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: spanSource, ...(spanSource === 'route' && { [URL_TEMPLATE]: newLocation }), }, diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 7a4ebaad3877..7335d0465bda 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -1,16 +1,10 @@ import type { Client } from '@sentry/core'; -import { - debug, - hasSpanStreamingEnabled, - PAGELOAD_SPAN_NAME_FALLBACK, - parseBaggageHeader, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { debug, hasSpanStreamingEnabled, PAGELOAD_SPAN_NAME_FALLBACK, parseBaggageHeader } from '@sentry/core'; import { startBrowserTracingPageLoadSpan, WINDOW } from '@sentry/react'; import type { NEXT_DATA } from 'next/dist/shared/lib/utils'; import type { ParsedUrlQuery } from 'querystring'; import { DEBUG_BUILD } from '../../common/debug-build'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { PAGELOAD } from '@sentry/conventions/op'; const globalObject = WINDOW; @@ -113,7 +107,7 @@ export function pagesRouterInstrumentPageLoad(client: Client): void { name, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', + [SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', [SENTRY_SEGMENT_NAME_SOURCE]: route ? 'route' : 'url', ...(route && { [URL_TEMPLATE]: route }), ...(params && { ...params }), diff --git a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts index a2d2ab9ac0d2..6cb11040038f 100644 --- a/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts +++ b/packages/nextjs/src/common/utils/dropMiddlewareTunnelRequests.ts @@ -1,6 +1,6 @@ -import { HTTP_TARGET, URL_FULL, URL_PATH } from '@sentry/conventions/attributes'; +import { HTTP_TARGET, URL_FULL, URL_PATH, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { RawAttributes } from '@sentry/core'; -import { getClient, GLOBAL_OBJ, isSentryRequestUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span } from '@sentry/core'; +import { getClient, GLOBAL_OBJ, isSentryRequestUrl, type Span } from '@sentry/core'; import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes'; import { isPathnameUnderSentryTunnelRoute } from './tunnelPathnameMatch'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; @@ -32,7 +32,7 @@ export function dropMiddlewareTunnelRequests( const isMiddleware = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute'; // The fetch span could be originating from rewrites re-writing a tunnel request // So we want to filter it out - const isFetchSpan = attrs?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.node_fetch'; + const isFetchSpan = attrs?.[SENTRY_ORIGIN] === 'auto.http.node_fetch'; const isBaseServerHandleRequest = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest'; // If the span is not a middleware span, fetch span, or BaseServer.handleRequest span, return diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 29b2ac5c757a..e660682282d6 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -8,7 +8,6 @@ import { getIsolationScope, handleCallbackErrors, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan, @@ -27,6 +26,7 @@ import { SENTRY_KIND, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; @@ -138,7 +138,7 @@ async function withServerActionInstrumentationImplementation { diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 564b8c4f39e6..b9b53f9c7667 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -10,7 +10,6 @@ import { hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, registerSpanErrorInstrumentation, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; @@ -40,6 +39,7 @@ import { SENTRY_KIND, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; @@ -158,7 +158,7 @@ export function init(options: VercelEdgeOptions = {}): void { // Mark all spans generated by Next.js as 'auto' & server if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + span.setAttribute(SENTRY_ORIGIN, 'auto'); span.setAttribute(SENTRY_KIND, 'server'); } diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/handleOnSpanStart.ts index 5a7f3ea59772..e2e3bb933d6f 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/handleOnSpanStart.ts @@ -3,6 +3,7 @@ import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { Client, Span } from '@sentry/core'; import { @@ -10,7 +11,6 @@ import { getRootSpan, hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; @@ -96,13 +96,13 @@ export function handleOnSpanStart(span: Span, client: Client): void { [SENTRY_SEGMENT_NAME_SOURCE]: 'route', }); } - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + span.setAttribute(SENTRY_ORIGIN, 'auto'); } // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans // with patterns (e.g. http.server spans) that will produce confusing data. if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + span.setAttribute(SENTRY_ORIGIN, 'auto'); } if (isRootSpan) { diff --git a/packages/nitro/src/runtime/hooks/captureStorageEvents.ts b/packages/nitro/src/runtime/hooks/captureStorageEvents.ts index de1b9399efdf..2b768d93226d 100644 --- a/packages/nitro/src/runtime/hooks/captureStorageEvents.ts +++ b/packages/nitro/src/runtime/hooks/captureStorageEvents.ts @@ -1,5 +1,5 @@ import * as dc from 'node:diagnostics_channel'; -import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import { CACHE_OPERATION_NAMES, @@ -7,7 +7,6 @@ import { GLOBAL_OBJ, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; @@ -95,7 +94,7 @@ function setupStorageTracingChannel(operation: TracedOperation): void { attributes: { [SENTRY_OP]: OPERATION_SPAN_OPS[operation], [CACHE_OPERATION]: cacheOperationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [CACHE_KEY]: cacheKeys.length > 1 ? cacheKeys : cacheKeys[0], 'db.operation.name': operation, 'db.collection.name': mountBase(data), diff --git a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts index da1f517acc7c..2752e2f63d79 100644 --- a/packages/nitro/src/runtime/hooks/captureTracingEvents.ts +++ b/packages/nitro/src/runtime/hooks/captureTracingEvents.ts @@ -1,5 +1,5 @@ import * as dc from 'node:diagnostics_channel'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; import { isObjectLike, @@ -12,7 +12,6 @@ import { httpHeadersToSpanAttributes, HTTP_SPAN_NAME_FALLBACK, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, type Span, startInactiveSpan, @@ -121,7 +120,7 @@ function setupH3TracingChannels(): void { : spanName, attributes: { ...urlAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.h3', + [SENTRY_ORIGIN]: 'auto.http.nitro.h3', [SENTRY_OP]: data?.type === 'middleware' ? MIDDLEWARE : HTTP_SERVER, }, }); @@ -199,7 +198,7 @@ function setupSrvxTracingChannels(): void { attributes: { ...urlAttributes, ...headerAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx', + [SENTRY_ORIGIN]: 'auto.http.nitro.srvx', [SENTRY_OP]: data.middleware ? MIDDLEWARE : HTTP_SERVER, 'server.port': data.server.options.port, }, @@ -238,7 +237,7 @@ function setupSrvxTracingChannels(): void { name: `${data.middleware?.handler.name ?? 'unknown'} - ${data.request.method} ${data.request._url?.pathname}`, attributes: { ...urlAttributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nitro.srvx', + [SENTRY_ORIGIN]: 'auto.http.nitro.srvx', [SENTRY_OP]: MIDDLEWARE, }, parentSpan: requestParentSpans.get(data.request) || undefined, diff --git a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts index f5adf02ea0eb..270654bb9ff8 100644 --- a/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts +++ b/packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts @@ -1,11 +1,10 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import { captureException, debug, getClient, httpHeadersToSpanAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, type SpanAttributes, @@ -134,7 +133,7 @@ function withSpan( captureException(error, { mechanism: { handled: false, - type: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + type: attributes[SENTRY_ORIGIN], }, }); @@ -166,7 +165,7 @@ function getSpanAttributes( const attributes: SpanAttributes = { [SENTRY_OP]: MIDDLEWARE, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nuxt', + [SENTRY_ORIGIN]: 'auto.middleware.nuxt', 'nuxt.middleware.name': middlewareName, 'nuxt.middleware.hook.name': hookName ?? 'handler', }; diff --git a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts index 8584d7ec123c..71149f91ff19 100644 --- a/packages/nuxt/src/runtime/utils/instrumentDatabase.ts +++ b/packages/nuxt/src/runtime/utils/instrumentDatabase.ts @@ -1,4 +1,4 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; import { addBreadcrumb, @@ -7,7 +7,6 @@ import { debug, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, type Span, SPAN_STATUS_ERROR, startSpan, @@ -31,7 +30,7 @@ const patchedStatement = new WeakSet(); /** * The Sentry origin for the database plugin. */ -const SENTRY_ORIGIN = 'auto.db.nuxt'; +const NUXT_DATABASE_ORIGIN = 'auto.db.nuxt'; /** * Creates the Nitro database plugin setup by instrumenting the configured database instances. @@ -230,7 +229,7 @@ function handleSpanStart(fn: () => unknown, breadcrumbOpts?: { query: string; di captureException(error, { mechanism: { handled: false, - type: SENTRY_ORIGIN, + type: NUXT_DATABASE_ORIGIN, }, }); @@ -275,7 +274,7 @@ function createStartSpanOptions( attributes: { [DB_QUERY_TEXT]: queryText, [DB_QUERY_SUMMARY]: querySummary, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, + [SENTRY_ORIGIN]: NUXT_DATABASE_ORIGIN, [SENTRY_OP]: DB_QUERY, ...data, }, diff --git a/packages/nuxt/src/runtime/utils/instrumentStorage.ts b/packages/nuxt/src/runtime/utils/instrumentStorage.ts index 7d6cc98177fc..1480c1b7b32d 100644 --- a/packages/nuxt/src/runtime/utils/instrumentStorage.ts +++ b/packages/nuxt/src/runtime/utils/instrumentStorage.ts @@ -1,4 +1,4 @@ -import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CACHE_HIT, CACHE_KEY, CACHE_OPERATION, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import { CACHE_OPERATION_NAMES, @@ -7,7 +7,6 @@ import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, type SpanAttributes, @@ -187,7 +186,7 @@ function createMethodWrapper( captureException(error, { mechanism: { handled: false, - type: options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN], + type: options.attributes?.[SENTRY_ORIGIN], }, }); @@ -239,7 +238,7 @@ function createSpanStartOptions( const attributes: SpanAttributes = { [SENTRY_OP]: cacheOperation, [CACHE_OPERATION]: cacheOperationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.cache.nuxt', + [SENTRY_ORIGIN]: 'auto.cache.nuxt', [CACHE_KEY]: keys.length > 1 ? keys : keys[0], 'db.operation.name': methodName, 'db.collection.name': mountBase.replace(/:$/, ''), diff --git a/packages/react-router/src/client/createClientInstrumentation.ts b/packages/react-router/src/client/createClientInstrumentation.ts index faa39b4fbe75..fb77746120fc 100644 --- a/packages/react-router/src/client/createClientInstrumentation.ts +++ b/packages/react-router/src/client/createClientInstrumentation.ts @@ -9,7 +9,6 @@ import { GLOBAL_OBJ, hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, SPAN_STATUS_ERROR, updateSpanName, @@ -32,6 +31,7 @@ import { SENTRY_OP, URL_FULL, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, MIDDLEWARE, NAVIGATION } from '@sentry/conventions/op'; @@ -119,7 +119,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', 'navigation.type': 'browser.popstate', }, }, @@ -164,7 +164,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', 'navigation.type': navigationType, }, }, @@ -211,7 +211,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.navigation.react_router.instrumentation_api', 'navigation.type': 'router.navigate', }, }, @@ -244,7 +244,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'fetcher', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the key. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: description }), }, @@ -286,7 +286,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'clientLoader', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -320,7 +320,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'clientAction', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -356,7 +356,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: MIDDLEWARE, [CODE_FUNCTION_NAME]: 'clientMiddleware', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', 'react_router.route.id': routeId, 'http.route': routePattern, 'react_router.middleware.index': middlewareIndex, @@ -381,7 +381,7 @@ export function createSentryClientInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'lazy', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', }, }, async span => { diff --git a/packages/react-router/src/client/hydratedRouter.ts b/packages/react-router/src/client/hydratedRouter.ts index 70b8e195f090..a0bc90ff72e4 100644 --- a/packages/react-router/src/client/hydratedRouter.ts +++ b/packages/react-router/src/client/hydratedRouter.ts @@ -9,7 +9,6 @@ import { hasSpanStreamingEnabled, isThenable, NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import type { DataRouter } from 'react-router'; @@ -22,7 +21,13 @@ import { resolveNavigateArg, updateSpanWithParameterizedRoute, } from './utils'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_PATH, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + URL_PATH, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { NAVIGATION } from '@sentry/conventions/op'; const GLOBAL_OBJ_WITH_DATA_ROUTER = GLOBAL_OBJ as typeof GLOBAL_OBJ & { @@ -58,7 +63,7 @@ export function instrumentHydratedRouter(): void { // this event is for the currently active pageload normalizePathname(router.state.location.pathname) === normalizePathname(pageloadPath) ) { - pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.pageload.react_router'); + pageloadSpan.setAttribute(SENTRY_ORIGIN, 'auto.pageload.react_router'); updateSpanWithParameterizedRoute(pageloadSpan, router.state); } } @@ -187,7 +192,7 @@ function maybeCreateNavigationTransaction(name: string, url: string, source: 'ur attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: source, [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react_router', + [SENTRY_ORIGIN]: 'auto.navigation.react_router', ...(source === 'route' ? { [URL_TEMPLATE]: name } : {}), }, }, diff --git a/packages/react-router/src/server/createServerInstrumentation.ts b/packages/react-router/src/server/createServerInstrumentation.ts index f6fdee0e0133..ebf16d85c87d 100644 --- a/packages/react-router/src/server/createServerInstrumentation.ts +++ b/packages/react-router/src/server/createServerInstrumentation.ts @@ -7,6 +7,7 @@ import { SENTRY_OP, URL_FULL, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; import { @@ -17,7 +18,6 @@ import { getRootSpan, hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, updateSpanName, @@ -82,7 +82,7 @@ export function createSentryServerInstrumentation( updateSpanName(existingRootSpan, unparameterizedName); existingRootSpan.setAttributes({ [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: filterCollectedUrl(info.request.url), [URL_PATH]: pathname, @@ -106,7 +106,7 @@ export function createSentryServerInstrumentation( name: unparameterizedName, attributes: { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.http.react_router.instrumentation_api', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [HTTP_REQUEST_METHOD]: info.request.method, [URL_PATH]: pathname, @@ -156,7 +156,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'loader', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -191,7 +191,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'action', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), }, @@ -237,7 +237,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: MIDDLEWARE, [CODE_FUNCTION_NAME]: 'middleware', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', 'react_router.route.id': routeId, [HTTP_ROUTE]: routePattern, ...(middlewareName && { 'react_router.middleware.name': middlewareName }), @@ -264,7 +264,7 @@ export function createSentryServerInstrumentation( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'lazy', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', + [SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api', }, }, async span => { diff --git a/packages/react-router/src/server/wrapSentryHandleRequest.ts b/packages/react-router/src/server/wrapSentryHandleRequest.ts index f40d56cad735..2ffeb9102cbf 100644 --- a/packages/react-router/src/server/wrapSentryHandleRequest.ts +++ b/packages/react-router/src/server/wrapSentryHandleRequest.ts @@ -1,11 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { - getActiveSpan, - getCurrentScope, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - updateSpanName, -} from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { getActiveSpan, getCurrentScope, getRootSpan, updateSpanName } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; import type { AppLoadContext, EntryContext, RouterContextProvider } from 'react-router'; import { registerServerBuildGlobal } from './serverBuild'; @@ -95,7 +89,7 @@ export function wrapSentryHandleRequest( rootSpan.setAttributes({ [HTTP_ROUTE]: routeName, [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); } } diff --git a/packages/react-router/test/server/wrapSentryHandleRequest.test.ts b/packages/react-router/test/server/wrapSentryHandleRequest.test.ts index 6fa1f2ef1026..7fa8b1066cfb 100644 --- a/packages/react-router/test/server/wrapSentryHandleRequest.test.ts +++ b/packages/react-router/test/server/wrapSentryHandleRequest.test.ts @@ -1,6 +1,6 @@ import { PassThrough } from 'node:stream'; -import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { getActiveSpan, getRootSpan, getTraceMetaTags, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { getActiveSpan, getRootSpan, getTraceMetaTags } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { getMetaTagTransformer } from '../../src/server/getMetaTagTransformer'; @@ -70,7 +70,7 @@ describe('wrapSentryHandleRequest', () => { expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({ [HTTP_ROUTE]: '/some-path', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); }); @@ -90,7 +90,7 @@ describe('wrapSentryHandleRequest', () => { expect(mockRootSpan.setAttributes).toHaveBeenLastCalledWith({ [HTTP_ROUTE]: '/', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); const nestedIndexContext = { @@ -109,7 +109,7 @@ describe('wrapSentryHandleRequest', () => { expect(mockRootSpan.setAttributes).toHaveBeenLastCalledWith({ [HTTP_ROUTE]: '/dashboard', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', + [SENTRY_ORIGIN]: 'auto.http.react_router.request_handler', }); }); diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 7ee7d7654025..80132fd4a254 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -7,7 +7,6 @@ import { hasSpanStreamingEnabled, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '@sentry/core'; import { isNodeEnv } from '@sentry/core/server'; import type { BrowserClient, browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/react'; @@ -15,7 +14,7 @@ import { getClient, startBrowserTracingNavigationSpan, startBrowserTracingPageLo import * as React from 'react'; import { DEBUG_BUILD } from '../utils/debug-build'; import { hasManifest, maybeParameterizeRemixRoute } from './remixRouteParameterization'; -import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD } from '@sentry/conventions/op'; export type Params = { @@ -104,7 +103,7 @@ export function startPageloadSpan(client: Client): void { name: source === 'route' || !hasSpanStreamingEnabled(client) ? spanName : PAGELOAD_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.remix', + [SENTRY_ORIGIN]: 'auto.pageload.remix', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: spanName }), }, @@ -129,7 +128,7 @@ function startNavigationSpan(matches: RouteMatch[], location: ReturnType name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : NAVIGATION_SPAN_NAME_FALLBACK, attributes: { [SENTRY_OP]: NAVIGATION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix', + [SENTRY_ORIGIN]: 'auto.navigation.remix', [SENTRY_SEGMENT_NAME_SOURCE]: source, ...(source === 'route' && { [URL_TEMPLATE]: name }), }, diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index 6ec3c819c9f2..ca31c9f3894d 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -25,7 +25,6 @@ import { hasSpanStreamingEnabled, HTTP_SPAN_NAME_FALLBACK, httpHeadersToSpanAttributes, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, spanToJSON, startSpan, @@ -49,6 +48,7 @@ import { SENTRY_OP, URL_FULL, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HTTP_SERVER } from '@sentry/conventions/op'; @@ -145,7 +145,7 @@ function makeWrappedDocumentRequestFunction(instrumentTracing?: boolean) { attributes: { method: request.method, [URL_FULL]: filterCollectedUrl(request.url), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix', + [SENTRY_ORIGIN]: 'auto.function.remix', [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: 'documentRequest', @@ -231,7 +231,7 @@ function makeWrappedDataFunction( // module id stays on `router.navigation.route.id`. name: client && hasSpanStreamingEnabled(client) ? name : id, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix', + [SENTRY_ORIGIN]: 'auto.ui.remix', [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: id, [CODE_FUNCTION_NAME]: name, @@ -415,7 +415,7 @@ function wrapRequestHandler ServerBuild | Promise { name: spanName, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix', + [SENTRY_ORIGIN]: 'auto.http.remix', [SENTRY_SEGMENT_NAME_SOURCE]: source, [SENTRY_OP]: HTTP_SERVER, [URL_FULL]: filterCollectedUrl(url.href), diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 89c1216c9857..6ab1645ca503 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -9,7 +9,6 @@ import { isObjectLike, isURLObjectRelative, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, startInactiveSpan, waitForTracingChannelBinding, @@ -28,6 +27,7 @@ import { SENTRY_OP, HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HTTP_SERVER } from '@sentry/conventions/op'; import { remixChannels } from '@sentry/server-utils/orchestrion/config'; @@ -161,7 +161,7 @@ function subscribeRequestHandler(): void { : 'remix.request', attributes: { [SENTRY_KIND]: 'server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: HTTP_SERVER, ...(hasUrlName && { [SENTRY_SEGMENT_NAME_SOURCE]: 'url' }), [CODE_FUNCTION_NAME]: 'requestHandler', @@ -201,7 +201,7 @@ function subscribeCallRouteLoader(): void { // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? 'loader' : description, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: 'loader', @@ -240,7 +240,7 @@ function subscribeCallRouteAction(formDataCapture: FormDataCapture | undefined): // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? 'action' : description, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, [SENTRY_DESCRIPTION]: description, [CODE_FUNCTION_NAME]: 'action', diff --git a/packages/remix/src/v3/client/browserTracingIntegration.ts b/packages/remix/src/v3/client/browserTracingIntegration.ts index 38cd64183fb0..2975aa5e8c81 100644 --- a/packages/remix/src/v3/client/browserTracingIntegration.ts +++ b/packages/remix/src/v3/client/browserTracingIntegration.ts @@ -3,14 +3,8 @@ import { startBrowserTracingNavigationSpan, WINDOW, } from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -import { - type Client, - hasSpanStreamingEnabled, - type Integration, - NAVIGATION_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { type Client, hasSpanStreamingEnabled, type Integration, NAVIGATION_SPAN_NAME_FALLBACK } from '@sentry/core'; type Options = Parameters[0]; @@ -57,7 +51,7 @@ function instrumentNavigationApi(client: Client): void { name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathnameOf(url) || '/', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.remix_v3', + [SENTRY_ORIGIN]: 'auto.navigation.remix_v3', }, }, // The span needs the destination: `location` still points at the previous page until the diff --git a/packages/solidstart/src/server/withServerActionInstrumentation.ts b/packages/solidstart/src/server/withServerActionInstrumentation.ts index 2c572a4f0e2a..785ab1432f91 100644 --- a/packages/solidstart/src/server/withServerActionInstrumentation.ts +++ b/packages/solidstart/src/server/withServerActionInstrumentation.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR } from '@sentry/core'; +import { handleCallbackErrors, SPAN_STATUS_ERROR } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; import { captureException, getActiveSpan, spanToJSON, startSpan } from '@sentry/node'; import { isRedirect } from './utils'; @@ -9,6 +9,7 @@ import { HTTP_TARGET, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import { setHttpServerSpanRouteAttribute } from '@sentry/server-utils'; @@ -48,7 +49,7 @@ export async function withServerActionInstrumentation { attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -100,7 +100,7 @@ describe('solidRouterBrowserTracingIntegration', () => { attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SENTRY_ORIGIN]: 'auto.pageload.browser', }), }), ); @@ -145,7 +145,7 @@ describe('solidRouterBrowserTracingIntegration', () => { expect(span.attributes).toMatchObject({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', + [SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', }); for (const [key, value] of Object.entries(expectedParams as Record)) { @@ -181,7 +181,7 @@ describe('solidRouterBrowserTracingIntegration', () => { attributes: expect.objectContaining({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', + [SENTRY_ORIGIN]: 'auto.navigation.solidstart.solidrouter', }), }), ); diff --git a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts index 2fe8b328b9ad..1a908c0908b9 100644 --- a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts +++ b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts @@ -1,14 +1,13 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + CODE_FUNCTION_NAME, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import * as SentryCore from '@sentry/core'; import * as SentryCoreServer from '@sentry/core/server'; import * as SentryNode from '@sentry/node'; -import { - createTransport, - NodeClient, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - setCurrentClient, - spanToJSON, -} from '@sentry/node'; +import { createTransport, NodeClient, setCurrentClient, spanToJSON } from '@sentry/node'; import { redirect } from '@solidjs/router'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { withServerActionInstrumentation } from '../../src/server'; @@ -103,7 +102,7 @@ describe('withServerActionInstrumentation', () => { [SENTRY_OP]: 'function', [CODE_FUNCTION_NAME]: 'getPrefecture', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SENTRY_ORIGIN]: 'auto.function.solidstart', }), }), ); diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 4ddeaad8ffef..a1a78995ce0e 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -4,7 +4,6 @@ import { handleCallbackErrors, hasSpanStreamingEnabled, objectify, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '@sentry/core'; import { startSpan } from '@sentry/core/browser'; import { @@ -14,6 +13,7 @@ import { SENTRY_OP, URL_PATH, URL_TEMPLATE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import { captureException } from '@sentry/svelte'; @@ -95,7 +95,7 @@ export function wrapLoadWithSentry any>(origLoad: T) attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url', [URL_PATH]: event.url.pathname, ...(routeId && { [URL_TEMPLATE]: routeId }), diff --git a/packages/sveltekit/src/client/svelte4BrowserTracing.ts b/packages/sveltekit/src/client/svelte4BrowserTracing.ts index b80651ad0734..19c0f3600b98 100644 --- a/packages/sveltekit/src/client/svelte4BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte4BrowserTracing.ts @@ -4,7 +4,6 @@ import { NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, } from '@sentry/core'; import { getCurrentScope, @@ -13,7 +12,7 @@ import { startInactiveSpan, WINDOW, } from '@sentry/svelte'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { NAVIGATION, PAGELOAD, ROUTER } from '@sentry/conventions/op'; import type { Navigation, Page } from '@sveltejs/kit'; // eslint-disable-next-line typescript/no-deprecated @@ -49,7 +48,7 @@ function _instrumentPageload(client: Client, pageStore: Readable): void { name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, attributes: { [SENTRY_OP]: PAGELOAD, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }); @@ -127,7 +126,7 @@ function _instrumentNavigations(client: Client, navigatingStore: Readable any>(origLoad: T) attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url', [URL_PATH]: event.url.pathname, [HTTP_ROUTE]: routeId, @@ -130,7 +125,7 @@ export function wrapServerLoadWithSentry any>(origSe attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', + [SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: routeId ? 'route' : 'url', [HTTP_REQUEST_METHOD]: event.request.method, [URL_PATH]: event.url.pathname, diff --git a/packages/sveltekit/src/server-common/serverRoute.ts b/packages/sveltekit/src/server-common/serverRoute.ts index 97f06bcbaf5c..4a62f5268820 100644 --- a/packages/sveltekit/src/server-common/serverRoute.ts +++ b/packages/sveltekit/src/server-common/serverRoute.ts @@ -3,7 +3,6 @@ import { FUNCTION_SPAN_NAME_FALLBACK, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; @@ -13,6 +12,7 @@ import { HTTP_ROUTE, SENTRY_DESCRIPTION, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import type { RequestEvent } from '@sveltejs/kit'; @@ -71,7 +71,7 @@ export function wrapServerRouteWithSentry( attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: httpMethod, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [HTTP_REQUEST_METHOD]: httpMethod, [HTTP_ROUTE]: routeId, // Relay infers the description from `code.function.name`, which would drop the route. diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index 587af53544af..c27d9db8ddb6 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -4,13 +4,12 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Span } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; import { writable } from 'svelte/store'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { navigating, page } from '$app/stores'; import { browserTracingIntegration } from '../../src/client'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the // `navigating` store for each test. @@ -115,7 +114,7 @@ describe('browserTracingIntegration', () => { name: '/', attributes: { 'sentry.op': 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }); @@ -219,7 +218,7 @@ describe('browserTracingIntegration', () => { name: '/users/[id]', attributes: { 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/[id]', 'sentry.sveltekit.navigation.from': '/users', @@ -234,7 +233,7 @@ describe('browserTracingIntegration', () => { name: 'SvelteKit Route Change', attributes: { [SENTRY_OP]: 'router', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + [SENTRY_ORIGIN]: 'auto.ui.sveltekit', 'sentry.sveltekit.navigation.from': '/users', 'sentry.sveltekit.navigation.to': '/users/[id]', 'sentry.sveltekit.navigation.type': 'link', @@ -364,7 +363,7 @@ describe('browserTracingIntegration', () => { attributes: { 'sentry.op': 'navigation', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [URL_TEMPLATE]: '/users/[id]', 'sentry.sveltekit.navigation.from': '/users/[id]', 'sentry.sveltekit.navigation.to': '/users/[id]', @@ -377,7 +376,7 @@ describe('browserTracingIntegration', () => { name: 'SvelteKit Route Change', attributes: { [SENTRY_OP]: 'router', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.sveltekit', + [SENTRY_ORIGIN]: 'auto.ui.sveltekit', 'sentry.sveltekit.navigation.from': '/users/[id]', 'sentry.sveltekit.navigation.to': '/users/[id]', }, diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index c071dc2c0fb8..5978a2bffc01 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -1,6 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Client } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; import type { Load } from '@sveltejs/kit'; @@ -108,7 +107,7 @@ describe('wrapLoadWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'url.template': '/users/[id]', @@ -138,7 +137,7 @@ describe('wrapLoadWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', }, @@ -204,7 +203,7 @@ describe('wrapLoadWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'url.template': '/users/[id]', @@ -225,7 +224,7 @@ describe('wrapLoadWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', 'sentry.description': '/users/123', diff --git a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts index f77d2c0f8195..eca41882dd76 100644 --- a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts +++ b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts @@ -4,9 +4,8 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Span } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentSvelteKitTracing } from '../../src/client/svelte5BrowserTracing'; @@ -83,7 +82,7 @@ describe('svelte5 browser tracing', () => { name: '/', attributes: { 'sentry.op': 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }, }); @@ -124,7 +123,7 @@ describe('svelte5 browser tracing', () => { name: '/users/[id]', attributes: expect.objectContaining({ 'sentry.op': 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.sveltekit', + [SENTRY_ORIGIN]: 'auto.navigation.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: '/users/[id]', }), diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index 0b258dd51ee3..2c53de829a3e 100644 --- a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -1,6 +1,5 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { SpanJSON, StreamedSpanJSON, TransactionEvent } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { describe, expect, it } from 'vitest'; import { _enhanceKitSpan, @@ -49,7 +48,7 @@ describe('svelteKitSpansIntegration', () => { expect(event.spans?.[0]?.op).toBe('function'); expect(event.spans?.[0]?.origin).toBe('auto.http.sveltekit'); expect(event.spans?.[0]?.data[SENTRY_OP]).toBe('function'); - expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(event.spans?.[0]?.data[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); describe('_enhanceKitSpan', () => { @@ -76,7 +75,7 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBe(op); expect(span.origin).toBe(origin); expect(span.data[SENTRY_OP]).toBe(op); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); + expect(span.data[SENTRY_ORIGIN]).toBe(origin); }); it("doesn't change spans from other origins", () => { @@ -90,7 +89,7 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBeUndefined(); expect(span.origin).toBeUndefined(); expect(span.data[SENTRY_OP]).toBeUndefined(); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + expect(span.data[SENTRY_ORIGIN]).toBeUndefined(); }); it("doesn't overwrite the sveltekit.handle.root span", () => { @@ -100,7 +99,7 @@ describe('svelteKitSpansIntegration', () => { origin: 'auto.http.sveltekit', data: { [SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SENTRY_ORIGIN]: 'auto.http.sveltekit', }, span_id: '123', trace_id: 'abc', @@ -110,7 +109,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpan(rootHandleSpan); expect(rootHandleSpan.data[SENTRY_OP]).toBe('http.server'); - expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(rootHandleSpan.data[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); expect(rootHandleSpan.op).toBe('http.server'); expect(rootHandleSpan.origin).toBe('auto.http.sveltekit'); }); @@ -120,7 +119,7 @@ describe('svelteKitSpansIntegration', () => { description: 'someOtherSpan', data: { [SENTRY_OP]: 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', + [SENTRY_ORIGIN]: 'auto.db.pg', }, op: 'db', origin: 'auto.db.pg', @@ -134,7 +133,7 @@ describe('svelteKitSpansIntegration', () => { expect(span.op).toBe('db'); expect(span.origin).toBe('auto.db.pg'); expect(span.data[SENTRY_OP]).toBe('db'); - expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); + expect(span.data[SENTRY_ORIGIN]).toBe('auto.db.pg'); }); it("doesn't overwrite already set ops or origins on sveltekit spans", () => { @@ -204,7 +203,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpanStreamed(span); expect(span.attributes?.[SENTRY_OP]).toBe(op); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe(origin); expect(span.attributes?.['sentry.description']).toBe(spanName); }); @@ -214,7 +213,7 @@ describe('svelteKitSpansIntegration', () => { _enhanceKitSpanStreamed(span); expect(span.attributes?.[SENTRY_OP]).toBeUndefined(); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + expect(span.attributes?.[SENTRY_ORIGIN]).toBeUndefined(); expect(span.attributes?.['sentry.description']).toBeUndefined(); }); @@ -223,14 +222,14 @@ describe('svelteKitSpansIntegration', () => { name: 'sveltekit.handle.root', attributes: { [SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SENTRY_ORIGIN]: 'auto.http.sveltekit', }, }); _enhanceKitSpanStreamed(rootHandleSpan); expect(rootHandleSpan.attributes?.[SENTRY_OP]).toBe('http.server'); - expect(rootHandleSpan.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(rootHandleSpan.attributes?.[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); it("doesn't enhance unrelated spans", () => { @@ -238,14 +237,14 @@ describe('svelteKitSpansIntegration', () => { name: 'someOtherSpan', attributes: { [SENTRY_OP]: 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', + [SENTRY_ORIGIN]: 'auto.db.pg', }, }); _enhanceKitSpanStreamed(span); expect(span.attributes?.[SENTRY_OP]).toBe('db'); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe('auto.db.pg'); }); it("doesn't overwrite already set ops or origins on sveltekit spans", () => { @@ -254,14 +253,14 @@ describe('svelteKitSpansIntegration', () => { name: 'sveltekit.resolve', attributes: { [SENTRY_OP]: 'custom.op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.custom.origin', + [SENTRY_ORIGIN]: 'auto.custom.origin', }, }); _enhanceKitSpanStreamed(span); expect(span.attributes?.[SENTRY_OP]).toBe('custom.op'); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.custom.origin'); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe('auto.custom.origin'); }); it("doesn't overwrite an already set description", () => { @@ -280,14 +279,14 @@ describe('svelteKitSpansIntegration', () => { name: 'sveltekit.resolve', attributes: { [SENTRY_OP]: 'custom.op', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', }, }); _enhanceKitSpanStreamed(span); expect(span.attributes?.[SENTRY_OP]).toBe('custom.op'); - expect(span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(span.attributes?.[SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); }); }); diff --git a/packages/sveltekit/test/server-common/load.test.ts b/packages/sveltekit/test/server-common/load.test.ts index 99433faeb3f3..97b3a6390c08 100644 --- a/packages/sveltekit/test/server-common/load.test.ts +++ b/packages/sveltekit/test/server-common/load.test.ts @@ -1,6 +1,5 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Client, Event } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { NodeClient, setCurrentClient } from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; @@ -168,7 +167,7 @@ describe('wrapLoadWithSentry calls `startSpan`', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'http.route': '/users/[id]', @@ -189,7 +188,7 @@ describe('wrapLoadWithSentry calls `startSpan`', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', }, @@ -257,7 +256,7 @@ describe('wrapServerLoadWithSentry calls `startSpan`', () => { expect(transaction.contexts?.trace).toEqual({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', + [SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [SENTRY_OP]: 'function', 'code.function.name': 'load', @@ -358,7 +357,7 @@ describe('with span streaming enabled', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'url.path': '/users/123', 'http.route': '/users/[id]', @@ -379,7 +378,7 @@ describe('with span streaming enabled', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', 'url.path': '/users/123', 'sentry.description': '/users/123', @@ -399,7 +398,7 @@ describe('with span streaming enabled', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'load', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit.server', + [SENTRY_ORIGIN]: 'auto.function.sveltekit.server', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', 'http.request.method': 'GET', 'url.path': '/users/123', diff --git a/packages/sveltekit/test/server-common/serverRoute.test.ts b/packages/sveltekit/test/server-common/serverRoute.test.ts index 5a4564565bb8..ef984fbb8af9 100644 --- a/packages/sveltekit/test/server-common/serverRoute.test.ts +++ b/packages/sveltekit/test/server-common/serverRoute.test.ts @@ -1,10 +1,10 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Client } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import type { NumericRange, RequestEvent } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, wrapServerRouteWithSentry } from '../../src/server'; +import { wrapServerRouteWithSentry } from '../../src/server'; describe('wrapServerRouteWithSentry', () => { const originalRouteHandler = vi.fn(); @@ -36,7 +36,7 @@ describe('wrapServerRouteWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', 'http.route': '/api/users/:id', }, @@ -59,7 +59,7 @@ describe('wrapServerRouteWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', }, name: 'GET Server Route', @@ -92,7 +92,7 @@ describe('wrapServerRouteWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', 'http.route': '/api/users/:id', 'sentry.description': 'GET /api/users/:id', @@ -114,7 +114,7 @@ describe('wrapServerRouteWithSentry', () => { attributes: { [SENTRY_OP]: 'function', 'code.function.name': 'GET', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.sveltekit', + [SENTRY_ORIGIN]: 'auto.function.sveltekit', 'http.request.method': 'GET', 'sentry.description': 'GET Server Route', }, diff --git a/packages/tanstackstart-react/src/server/utils.ts b/packages/tanstackstart-react/src/server/utils.ts index 5d58be067658..7f6793fc806b 100644 --- a/packages/tanstackstart-react/src/server/utils.ts +++ b/packages/tanstackstart-react/src/server/utils.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import type { StartSpanOptions } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; /** * Returns span options for TanStack Start middleware spans. @@ -10,7 +9,7 @@ export function getMiddlewareSpanOptions(name: string): StartSpanOptions { return { name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.tanstackstart', + [SENTRY_ORIGIN]: 'auto.middleware.tanstackstart', [SENTRY_OP]: MIDDLEWARE, }, }; diff --git a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts index 77029ebbf907..83f148a77771 100644 --- a/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts +++ b/packages/tanstackstart-react/src/server/wrapFetchWithSentry.ts @@ -1,7 +1,13 @@ import { getClient, getTraceMetaTags, hasSpanStreamingEnabled } from '@sentry/core'; import { flushIfServerless } from '@sentry/core/server'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/node'; -import { CODE_FUNCTION_NAME, HTTP_REQUEST_METHOD, SENTRY_DESCRIPTION, SENTRY_OP } from '@sentry/conventions/attributes'; +import { captureException, startSpan } from '@sentry/node'; +import { + CODE_FUNCTION_NAME, + HTTP_REQUEST_METHOD, + SENTRY_DESCRIPTION, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import { updateSpanWithRouteParametrization } from './routeParametrization'; @@ -154,7 +160,7 @@ export function wrapFetchWithSentry(serverEntry: ServerEntry): ServerEntry { // request path carries the generated server function id, which is high cardinality. name: hasSpanStreaming ? 'serverFn' : description, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.tanstackstart.server', + [SENTRY_ORIGIN]: 'auto.function.tanstackstart.server', [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: 'serverFn', // The global function middleware renames this span and needs the method, which it From 78d070ee6fa1795735c8c24de476265a36bfb71c Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 13:03:48 +0200 Subject: [PATCH 15/73] ref(server): Use SENTRY_ORIGIN convention constant (#25036) Uses `SENTRY_ORIGIN` from `@sentry/conventions/attributes` in server instrumentation, runtimes, and their tests. It has the same `sentry.origin` value; transmitted data and public exports are unchanged. Part of #24961 / [JS-3868](https://linear.app/getsentry/issue/JS-3868). Co-authored-by: GPT-6 --- .../suites/public-api/startSpan/test.ts | 13 ++-- .../suites/tracing/d1/test.ts | 5 +- .../suites/tracing/durableobject-sql/test.ts | 7 +- .../tracing/durableobject-sync-kv/test.ts | 5 +- .../suites/tracing/scheduled/test.ts | 6 +- .../suites/tracing/workflow/test.ts | 6 +- .../suites/fs-instrumentation/test.ts | 51 +++++++------- .../startSpan/basic-usage-streamed/test.ts | 11 +-- .../suites/tracing/groq/test.ts | 6 +- .../suites/tracing/langgraph/test.ts | 4 +- .../suites/tracing/mistral/test.ts | 12 ++-- .../tracing/openai/openai-tool-calls/test.ts | 18 ++--- .../suites/tracing/openai/test.ts | 68 +++++++++---------- .../suites/tracing/openai/v6/test.ts | 42 ++++++------ .../suites/tracing/openai/v7/test.ts | 10 +-- .../suites/tracing/together-ai/test.ts | 6 +- .../vercelai/span-streaming-v4/test.ts | 32 ++++----- .../vercelai/span-streaming-v6/test.ts | 32 ++++----- packages/bun/src/integrations/bunserver.ts | 4 +- .../agents/instrumentAgentCallableRpc.ts | 6 +- .../instrumentDurableObjectStorage.ts | 6 +- .../instrumentDurableObjectSyncKvStorage.ts | 6 +- .../instrumentations/instrumentSqlStorage.ts | 13 +--- .../instrumentations/worker/instrumentD1.ts | 15 ++-- .../worker/instrumentQueue.ts | 6 +- .../worker/instrumentQueueProducer.ts | 6 +- .../instrumentations/worker/instrumentR2.ts | 5 +- packages/cloudflare/src/workflows.ts | 19 +++--- .../instrumentDurableObjectStorage.test.ts | 26 +++---- ...strumentDurableObjectSyncKvStorage.test.ts | 10 +-- .../test/instrumentSqlStorage.test.ts | 10 +-- packages/deno/src/opentelemetry/tracer.ts | 7 +- packages/elysia/src/withElysia.ts | 8 +-- .../src/gcpfunction/cloud_events.ts | 4 +- .../src/gcpfunction/http.ts | 4 +- .../src/integrations/google-cloud-grpc.ts | 6 +- .../src/integrations/google-cloud-http.ts | 12 +++- .../test/gcpfunction/cloud_event.test.ts | 17 ++--- .../test/gcpfunction/events.test.ts | 17 ++--- .../test/gcpfunction/http.test.ts | 7 +- .../integrations/google-cloud-grpc.test.ts | 6 +- .../integrations/google-cloud-http.test.ts | 14 ++-- packages/nestjs/src/decorators.ts | 6 +- packages/nestjs/src/integrations/helpers.ts | 15 ++-- .../nestjs/src/integrations/wrap-route.ts | 8 +-- packages/nestjs/test/decorators.test.ts | 9 ++- .../test/integrations/span-origin.test.ts | 8 +-- .../fs/vendored/instrumentation.ts | 5 +- .../http/httpServerSpansIntegration.ts | 4 +- .../node-fetch/undici-instrumentation.ts | 4 +- packages/node/src/utils/addOriginToSpan.ts | 4 +- .../test/integration/transactions.test.ts | 9 ++- .../node/test/integrations/undici.test.ts | 4 +- .../server-utils/src/ai/anthropic-ai/index.ts | 12 +--- packages/server-utils/src/ai/flue/index.ts | 10 ++- packages/server-utils/src/ai/flue/utils.ts | 14 ++-- .../server-utils/src/ai/google-genai/index.ts | 4 +- .../src/ai/langchain/embeddings.ts | 11 +-- .../server-utils/src/ai/langchain/index.ts | 14 ++-- .../server-utils/src/ai/langchain/utils.ts | 5 +- .../server-utils/src/ai/langgraph/index.ts | 5 +- .../server-utils/src/ai/langgraph/utils.ts | 5 +- packages/server-utils/src/ai/mastra/index.ts | 5 +- packages/server-utils/src/ai/mistral/index.ts | 11 +-- packages/server-utils/src/ai/openai/index.ts | 4 +- .../server-utils/src/ai/typesafe/index.ts | 12 +--- .../server-utils/src/ai/workers-ai/utils.ts | 5 +- .../server-utils/src/integrations/amqplib.ts | 6 +- .../src/integrations/anthropic.ts | 5 +- .../src/integrations/aws-sdk/index.ts | 5 +- .../src/integrations/dataloader.ts | 4 +- .../integrations/express/instrumentation.ts | 5 +- .../integrations/fastify/instrumentation.ts | 6 +- .../src/integrations/firebase/firestore.ts | 5 +- .../src/integrations/firebase/functions.ts | 4 +- .../src/integrations/generic-pool.ts | 6 +- .../src/integrations/google-genai.ts | 3 +- .../graphql/graphql-dc-subscriber.ts | 17 ++--- .../src/integrations/graphql/resolvers.ts | 5 +- .../src/integrations/graphql/spans.ts | 11 +-- .../src/integrations/hapi/hapi-utils.ts | 5 +- .../src/integrations/hono/honoIntegration.ts | 6 +- .../src/integrations/hono/patchAppRequest.ts | 13 +--- .../integrations/hono/wrapMiddlewareSpan.ts | 5 +- .../src/integrations/kafkajs/spans.ts | 6 +- .../server-utils/src/integrations/knex.ts | 4 +- .../src/integrations/koa/index.ts | 5 +- .../server-utils/src/integrations/mistral.ts | 10 +-- .../src/integrations/mongodb/mongodb-span.ts | 11 +-- .../mongoose/mongoose-dc-subscriber.ts | 12 +--- .../mongoose/mongoose-legacy-span.ts | 5 +- .../server-utils/src/integrations/mysql.ts | 4 +- .../src/integrations/mysql2/index.ts | 4 +- .../mysql2/mysql2-dc-subscriber.ts | 7 +- .../src/integrations/openai-compatible.ts | 5 +- .../server-utils/src/integrations/openai.ts | 4 +- .../src/integrations/postgres-js.ts | 4 +- .../server-utils/src/integrations/postgres.ts | 4 +- .../src/integrations/prisma/orchestrion.ts | 13 +--- .../src/integrations/prisma/tracing-helper.ts | 4 +- .../src/integrations/redis/index.ts | 6 +- .../redis/ioredis-channel-subscriber.ts | 5 +- .../integrations/redis/redis-dc-subscriber.ts | 9 +-- .../server-utils/src/integrations/tedious.ts | 4 +- .../vercel-ai/vercel-ai-dc-subscriber.ts | 6 +- .../test/ai/lib/tracing/workers-ai.test.ts | 6 +- .../ai/lib/utils/workers-ai-utils.test.ts | 6 +- 107 files changed, 468 insertions(+), 548 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts index 00d89567b577..0fa5d410368b 100644 --- a/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/public-api/startSpan/test.ts @@ -1,4 +1,4 @@ -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_SDK_NAME, @@ -10,6 +10,7 @@ import { SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; @@ -85,7 +86,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, }, name: 'test-child-span', @@ -104,7 +105,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, @@ -141,7 +142,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa attributes: { 'sentry.is_localhost': { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK }, [SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, @@ -169,7 +170,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, }, name: 'test-span', @@ -193,7 +194,7 @@ it('sends a streamed span envelope with correct spans for a manually started spa value: expect.arrayContaining(['SpanStreaming']), }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.cloudflare' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.cloudflare' }, [SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName }, [SENTRY_OP]: { type: 'string', value: 'http.server' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts index 13a6474f8377..9c88edd7b0a3 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts @@ -1,6 +1,6 @@ import { expect, it } from 'vitest'; import type { Envelope, SerializedStreamedSpan } from '@sentry/core'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; import { SENTRY_SDK_NAME, SENTRY_SDK_VERSION, @@ -9,6 +9,7 @@ import { SENTRY_TRACE_LIFECYCLE, SENTRY_ENVIRONMENT, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { createRunner } from '../../../runner'; import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; @@ -37,7 +38,7 @@ function commonAttributes(segmentSpan: SerializedStreamedSpan): SerializedStream [SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentSpan.name }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, [SENTRY_OP]: { type: 'string', value: 'db.query' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.d1' }, 'db.system.name': { type: 'string', value: 'cloudflare-d1' }, }; } diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts index 3b0fbb839714..1f8e514601a8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sql/test.ts @@ -1,6 +1,5 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Envelope } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; import { getSpansFromEnvelope } from '../../../spanUtils'; @@ -24,7 +23,7 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal expect(segmentSpan?.attributes['url.path']).toEqual({ type: 'string', value: '/exec' }); const sqlSpans = spans.filter( - span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value === 'auto.db.cloudflare.durable_object.sql', + span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.db.cloudflare.durable_object.sql', ); expect(sqlSpans).toHaveLength(3); @@ -34,7 +33,7 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal name: 'CREATE TABLE users', attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'db.query' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object.sql' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object.sql' }, 'db.system.name': { type: 'string', value: 'cloudflare-durable-object-sql' }, 'db.operation.name': { type: 'string', value: 'exec' }, 'db.query.text': { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts index 113d35d64cf8..ddbfc897054e 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject-sync-kv/test.ts @@ -1,7 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Envelope } from '@sentry/core'; import { expect, it } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { createRunner } from '../../../runner'; import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils'; @@ -31,7 +30,7 @@ it('instruments sync KV operations on Durable Object storage', async ({ signal } name: 'durable_object_storage_kv_put', attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'db' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.db.cloudflare.durable_object' }, 'db.system.name': { type: 'string', value: 'cloudflare-durable-object-sql' }, 'db.operation.name': { type: 'string', value: 'put' }, }), diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts index 637dddd01aa3..f894df8a361d 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/scheduled/test.ts @@ -1,7 +1,7 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; import type { Event } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { createRunner } from '../../../runner'; import { getSpansFromEnvelope } from '../../../spanUtils'; @@ -21,7 +21,7 @@ it('Scheduled handler creates a segment span with correct attributes', async ({ status: 'ok', attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'function' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.scheduled' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.scheduled' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'task' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, 'code.function.name': { type: 'string', value: 'scheduled' }, diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts index 5934cf064a1e..86bcab0fb7ce 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workflow/test.ts @@ -1,6 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect, it } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { createRunner } from '../../../runner'; it('Workflow steps create segment spans with correct attributes', async ({ signal }) => { @@ -25,7 +25,7 @@ it('Workflow steps create segment spans with correct attributes', async ({ signa status: 'ok', attributes: expect.objectContaining({ [SENTRY_OP]: { type: 'string', value: 'function' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.workflow' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.faas.cloudflare.workflow' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'task' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, 'code.function.name': { type: 'string', value: stepName }, diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts index 644a8c886e5e..e4d4a1474e05 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/test.ts @@ -1,6 +1,5 @@ import type { SerializedStreamedSpanContainer } from '@sentry/core'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/node'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createCjsTests } from '../../utils/runner'; @@ -24,11 +23,7 @@ function expectFileSpans( status, attributes: Object.fromEntries( Object.entries(attributes).filter( - ([key]) => - key.endsWith('_argument') || - key === 'error.type' || - key === SENTRY_OP || - key === SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + ([key]) => key.endsWith('_argument') || key === 'error.type' || key === SENTRY_OP || key === SENTRY_ORIGIN, ), ), })), @@ -61,7 +56,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -83,7 +78,7 @@ describe('fs instrumentation', () => { attributes: { path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -92,7 +87,7 @@ describe('fs instrumentation', () => { attributes: { path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file-promises.txt') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -104,7 +99,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -127,7 +122,7 @@ describe('fs instrumentation', () => { src_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, dest_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.copy') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -140,7 +135,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promises.txt.copy'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -153,7 +148,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promisify.txt.copy'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -176,7 +171,7 @@ describe('fs instrumentation', () => { existing_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, new_path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt.link') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -186,7 +181,7 @@ describe('fs instrumentation', () => { existing_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt') }, new_path_argument: { type: 'string', value: expect.stringMatching('/some-file-promises.txt.link') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -202,7 +197,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promisify.txt.link'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -224,7 +219,7 @@ describe('fs instrumentation', () => { attributes: { prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -233,7 +228,7 @@ describe('fs instrumentation', () => { attributes: { prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -242,7 +237,7 @@ describe('fs instrumentation', () => { attributes: { prefix_argument: { type: 'string', value: expect.stringMatching('/foo-') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -265,7 +260,7 @@ describe('fs instrumentation', () => { target_argument: { type: 'string', value: expect.stringMatching('/some-file-promisify.txt') }, path_argument: { type: 'string', value: expect.stringMatching('/some-file-promisify.txt.symlink') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -281,7 +276,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -297,7 +292,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promisify.txt.symlink'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -319,7 +314,7 @@ describe('fs instrumentation', () => { attributes: { path_argument: { type: 'string', value: expect.stringMatching('/fixtures/some-file.txt') }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, { @@ -331,7 +326,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-promisify.txt'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -367,7 +362,7 @@ describe('fs instrumentation', () => { value: expect.stringMatching('/fixtures/some-file-that-doesnt-exist.txt'), }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -400,7 +395,7 @@ describe('fs instrumentation', () => { attributes: { 'error.type': { type: 'string', value: 'ENOENT' }, [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), @@ -422,7 +417,7 @@ describe('fs instrumentation', () => { // Neither `path_argument` nor `error.type` are recorded attributes: { [SENTRY_OP]: { type: 'string', value: 'file' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.file.fs' }, }, }, ]), diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index dd7d18f7c961..87a40fa69b3f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -1,4 +1,4 @@ -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_SEGMENT_ID, @@ -10,6 +10,7 @@ import { SENTRY_RELEASE, SENTRY_SDK_INTEGRATIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { expect, test } from 'vitest'; import { createRunner } from '../../../../utils/runner'; @@ -66,7 +67,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, }, name: 'test-child-span', is_segment: false, @@ -90,7 +91,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, }, links: [ { @@ -127,7 +128,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, }, name: 'test-manual-span', is_segment: false, @@ -154,7 +155,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' }, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'manual' }, [SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'custom' }, 'process.runtime.engine.name': { type: 'string', value: 'v8' }, 'process.runtime.engine.version': { type: 'string', value: expect.any(String) }, diff --git a/dev-packages/node-integration-tests/suites/tracing/groq/test.ts b/dev-packages/node-integration-tests/suites/tracing/groq/test.ts index d63eca3f15c5..ab5271d177ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing/groq/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/groq/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -16,6 +15,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -39,7 +39,7 @@ describe('Groq integration', () => { expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('llama-3.3-70b-versatile'); expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE]?.value).toBe(0.7); @@ -89,7 +89,7 @@ describe('Groq integration', () => { 'embeddings nomic-embed-text-v1_5', ]); for (const span of rawSpans) { - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(span.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(span.attributes[GEN_AI_RESPONSE_MODEL]).toBeUndefined(); expect(span.attributes[GEN_AI_USAGE_TOTAL_TOKENS]).toBeUndefined(); expect(span.attributes[GEN_AI_RESPONSE_TEXT]).toBeUndefined(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index e436b94a61e8..92b94bf07853 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_AGENT_NAME, @@ -16,6 +15,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getStringAttributeValue } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -276,7 +276,7 @@ describe('LangGraph integration', () => { attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: expect.objectContaining({ value: 'invoke_agent' }), [SENTRY_OP]: expect.objectContaining({ value: 'gen_ai.invoke_agent' }), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), + [SENTRY_ORIGIN]: expect.objectContaining({ value: 'auto.ai.langgraph' }), [GEN_AI_AGENT_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), [GEN_AI_PIPELINE_NAME]: expect.objectContaining({ value: 'helpful_assistant' }), }), diff --git a/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts b/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts index 4ae126d350d9..1b1e91310beb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mistral/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_AGENT_NAME, GEN_AI_EMBEDDINGS_INPUT, @@ -21,6 +20,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmTests } from '../../../utils/runner'; @@ -49,7 +49,7 @@ describe('Mistral integration', () => { expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('mistral-small-latest'); expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE]?.value).toBe(0.7); @@ -142,7 +142,7 @@ describe('Mistral integration', () => { expect(embeddingsSpan!.status).toBe('ok'); expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('embeddings'); expect(embeddingsSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.embeddings'); - expect(embeddingsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(embeddingsSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(embeddingsSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(embeddingsSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('mistral-embed'); expect(embeddingsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(8); @@ -185,7 +185,7 @@ describe('Mistral integration', () => { expect(agentSpan!.status).toBe('ok'); expect(agentSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('invoke_agent'); expect(agentSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.invoke_agent'); - expect(agentSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(agentSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(agentSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(agentSpan!.attributes[GEN_AI_AGENT_NAME]?.value).toBe('ag-mock-123'); expect(agentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(10); @@ -263,7 +263,7 @@ describe('Mistral integration', () => { expect(chatSpan).toBeDefined(); expect(chatSpan!.name).toBe('chat mistral-small-latest'); expect(chatSpan!.status).toBe('ok'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_RESPONSE_TEXT]?.value).toBe('["Hello from the manual client!"]'); @@ -298,7 +298,7 @@ describe('Mistral integration', () => { expect(parseSpans[0]!.name).toBe('chat mistral-small-latest'); expect(parseSpans[0]!.status).toBe('ok'); expect(parseSpans[0]!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); - expect(parseSpans[0]!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(parseSpans[0]!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(parseSpans[0]!.attributes[GEN_AI_REQUEST_STREAM]?.value).toBe(false); expect(parseSpans[0]!.attributes[GEN_AI_USAGE_TOTAL_TOKENS]?.value).toBe(12); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts index cd746332059b..5dfd13c21f7f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -16,6 +15,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -97,7 +97,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -147,7 +147,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -207,7 +207,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -260,7 +260,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -333,7 +333,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -397,7 +397,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -468,7 +468,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -532,7 +532,7 @@ describe('OpenAI Tool Calls integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index 27382085cdcc..2da282ebabb4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { @@ -19,6 +18,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, @@ -55,7 +55,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -112,7 +112,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -162,7 +162,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -189,7 +189,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -252,7 +252,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -324,7 +324,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -345,7 +345,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -398,7 +398,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -471,7 +471,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -540,7 +540,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -598,7 +598,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -629,7 +629,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -704,7 +704,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -788,7 +788,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -809,7 +809,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -871,7 +871,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1024,7 +1024,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1069,7 +1069,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1098,7 +1098,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1145,7 +1145,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1192,7 +1192,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1243,7 +1243,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1276,7 +1276,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1317,7 +1317,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(rawEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(rawEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1362,7 +1362,7 @@ describe('OpenAI integration', () => { data: { [GEN_AI_OPERATION_NAME]: 'chat', [SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, @@ -1404,7 +1404,7 @@ describe('OpenAI integration', () => { data: { [GEN_AI_OPERATION_NAME]: 'chat', [SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, @@ -1451,7 +1451,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(conversationCreateSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(conversationCreateSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1480,7 +1480,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(conversationResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(conversationResponseSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1523,7 +1523,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(previousResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(previousResponseSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1554,7 +1554,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(rawConversationSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(rawConversationSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1754,7 +1754,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1784,7 +1784,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(parsedChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1814,7 +1814,7 @@ describe('OpenAI integration', () => { type: 'string', value: 'gen_ai.chat', }); - expect(parsedResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(parsedResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index be3746e90068..49d5cb48bd9d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -17,6 +16,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE, @@ -55,7 +55,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -110,7 +110,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -160,7 +160,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -187,7 +187,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -250,7 +250,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -322,7 +322,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -364,7 +364,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -431,7 +431,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -489,7 +489,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -520,7 +520,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -595,7 +595,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -679,7 +679,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -789,7 +789,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -834,7 +834,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -863,7 +863,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -929,7 +929,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -980,7 +980,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1013,7 +1013,7 @@ describe('OpenAI integration (V6)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -1077,7 +1077,7 @@ describe('OpenAI integration (V6)', () => { data: { [GEN_AI_OPERATION_NAME]: 'chat', [SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, @@ -1130,7 +1130,7 @@ describe('OpenAI integration (V6)', () => { data: { [GEN_AI_OPERATION_NAME]: 'chat', [SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: 'auto.ai.openai', [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_REQUEST_MODEL]: 'gpt-3.5-turbo', [GEN_AI_REQUEST_TEMPERATURE]: 0.7, diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts index 646b63ec7a6a..fbea79d4f34c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v7/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { GEN_AI_OPERATION_NAME, @@ -12,6 +11,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; import { conditionalTest } from '../../../../utils/index'; @@ -49,7 +49,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -97,7 +97,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { const errorSpan = container.items.find(span => span.name === 'chat error-model' && span.status !== 'ok'); expect(errorSpan).toBeDefined(); - expect(errorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -140,7 +140,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { type: 'string', value: 'gen_ai.embeddings', }); - expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -189,7 +189,7 @@ conditionalTest({ min: 22 })('OpenAI integration (V7)', () => { type: 'string', value: 'gen_ai.chat', }); - expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); diff --git a/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts b/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts index b9a7fb1fe38b..1e912a358fc0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/together-ai/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, @@ -16,6 +15,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; @@ -39,7 +39,7 @@ describe('Together integration', () => { expect(chatSpan!.status).toBe('ok'); expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME]?.value).toBe('chat'); expect(chatSpan!.attributes[SENTRY_OP]?.value).toBe('gen_ai.chat'); - expect(chatSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(chatSpan!.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME]?.value).toBe(PROVIDER); expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL]?.value).toBe('meta-llama/Llama-3.3-70B-Instruct-Turbo'); expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE]?.value).toBe(0.7); @@ -89,7 +89,7 @@ describe('Together integration', () => { 'embeddings togethercomputer/m2-bert-80M-8k-retrieval', ]); for (const span of rawSpans) { - expect(span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]?.value).toBe(ORIGIN); + expect(span.attributes[SENTRY_ORIGIN]?.value).toBe(ORIGIN); expect(span.attributes[GEN_AI_RESPONSE_MODEL]).toBeUndefined(); expect(span.attributes[GEN_AI_USAGE_TOTAL_TOKENS]).toBeUndefined(); expect(span.attributes[GEN_AI_RESPONSE_TEXT]).toBeUndefined(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts index 3da2f82f24c9..50385f0e0c40 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v4/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -15,6 +14,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -52,7 +52,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Second span - generate_content for simple generateText @@ -67,7 +67,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Third span - invoke_agent for explicit telemetry generateText @@ -81,7 +81,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fourth span - tool call invoke_agent @@ -95,7 +95,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fifth span - tool call generate_content @@ -109,7 +109,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Sixth span - execute_tool @@ -122,7 +122,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), [SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), ]), @@ -146,7 +146,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Second span - generate_content with input/output messages @@ -165,7 +165,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Third span - explicit telemetry invoke_agent with messages @@ -183,7 +183,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fourth span - tool call invoke_agent with messages @@ -201,7 +201,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Fifth span - tool call generate_content with tool definitions @@ -218,7 +218,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), // Sixth span - execute_tool with description and input/output @@ -233,7 +233,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_CALL_RESULT]: expect.objectContaining({ value: expect.any(String) }), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), [SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), ]), @@ -247,7 +247,7 @@ describe('Vercel AI integration (streaming v4)', () => { attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), expect.objectContaining({ @@ -260,7 +260,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), expect.objectContaining({ @@ -271,7 +271,7 @@ describe('Vercel AI integration (streaming v4)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), [SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(expectedOrigin), + [SENTRY_ORIGIN]: attr(expectedOrigin), }), }), ]), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts index 806649ede43f..db4707374c4e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES, @@ -15,6 +14,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../../../../../../packages/server-utils/src/ai/core/gen-ai-attributes'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -49,7 +49,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Second span - generate_content for simple generateText @@ -64,7 +64,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Third span - invoke_agent for explicit telemetry generateText @@ -78,7 +78,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fourth span - tool call invoke_agent @@ -92,7 +92,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fifth span - tool call generate_content @@ -106,7 +106,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Sixth span - execute_tool @@ -119,7 +119,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), [SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), ]), @@ -143,7 +143,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Second span - generate_content with input/output messages @@ -162,7 +162,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Third span - explicit telemetry invoke_agent with messages @@ -180,7 +180,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(30), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fourth span - tool call invoke_agent with messages (V6: no text part, only tool_call) @@ -198,7 +198,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Fifth span - tool call generate_content with tool definitions @@ -215,7 +215,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), // Sixth span - execute_tool with description and input/output @@ -230,7 +230,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_CALL_RESULT]: expect.objectContaining({ value: expect.any(String) }), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), [SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), ]), @@ -243,7 +243,7 @@ describe('Vercel AI integration (streaming, v6)', () => { attributes: expect.objectContaining({ [GEN_AI_OPERATION_NAME]: attr('invoke_agent'), [SENTRY_OP]: attr('gen_ai.invoke_agent'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), expect.objectContaining({ @@ -256,7 +256,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_USAGE_TOTAL_TOKENS]: attr(40), [GEN_AI_OPERATION_NAME]: attr('generate_content'), [SENTRY_OP]: attr('gen_ai.generate_content'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), expect.objectContaining({ @@ -267,7 +267,7 @@ describe('Vercel AI integration (streaming, v6)', () => { [GEN_AI_TOOL_NAME]: attr('getWeather'), [GEN_AI_OPERATION_NAME]: attr('execute_tool'), [SENTRY_OP]: attr('gen_ai.execute_tool'), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: attr(origin), + [SENTRY_ORIGIN]: attr(origin), }), }), ]), diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 080251c7dba3..bf73c591d467 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -12,7 +12,6 @@ import { HTTP_SPAN_NAME_FALLBACK, isURLObjectRelative, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, startSpan, winterCGRequestToRequestData, @@ -36,6 +35,7 @@ import { URL_PORT, URL_QUERY, URL_SCHEME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; @@ -355,7 +355,7 @@ function getSpanAttributesFromParsedUrl( request: Request, ): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.bun.serve', + [SENTRY_ORIGIN]: 'auto.http.bun.serve', [HTTP_REQUEST_METHOD]: request.method || 'GET', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', }; diff --git a/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts b/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts index bf64488ad90c..88ea17db8eb5 100644 --- a/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts +++ b/packages/cloudflare/src/instrumentations/agents/instrumentAgentCallableRpc.ts @@ -1,6 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; -import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { debug, startSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { AGENT_SPAN_ORIGIN, type AgentInternals, getAgentAttributes, setAgentConversationId } from './types'; @@ -35,7 +35,7 @@ export function instrumentAgentCallableRpc(obj: AgentInternals): void { name: method, attributes: { [SENTRY_OP]: RPC, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: AGENT_SPAN_ORIGIN, + [SENTRY_ORIGIN]: AGENT_SPAN_ORIGIN, ...getAgentAttributes(thisArg), }, }, diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts index 976412bae83d..e1d1a3fc1025 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectStorage.ts @@ -1,7 +1,7 @@ import type { DurableObjectStorage, SyncKvStorage, SqlStorage } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { getClient, isThenable, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { getClient, isThenable, startSpan } from '@sentry/core'; import type { CloudflareClientOptions } from '../client'; import { getStorageKeys, targetsCloudflareInternalKey } from '../utils/internalStorageKey'; import { storeSpanContext } from '../utils/traceLinks'; @@ -75,7 +75,7 @@ export function instrumentDurableObjectStorage( name: `durable_object_storage_${methodName}`, attributes: { [SENTRY_OP]: DB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': methodName, }, diff --git a/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts b/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts index c8b94d14fef4..eb21b57a2fbf 100644 --- a/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentDurableObjectSyncKvStorage.ts @@ -1,7 +1,7 @@ import type { SyncKvStorage } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { startSpan } from '@sentry/core'; const SYNC_KV_METHODS_TO_INSTRUMENT = ['get', 'put', 'delete', 'list'] as const; @@ -28,7 +28,7 @@ export function instrumentDurableObjectSyncKvStorage(syncKv: SyncKvStorage): Syn name: `durable_object_storage_kv_${methodName}`, attributes: { [SENTRY_OP]: DB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', // keeping the value as close as possible to the Cloudflare Worker KV instrumentation // https://github.com/cloudflare/workerd/blob/6b8b11787e2b2a800ab0edd0690bfab3857b0529/src/workerd/api/sync-kv.c%2B%2B#L19 'db.system.name': 'cloudflare-durable-object-sql', diff --git a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts index 706960562f88..276cd0be5f8d 100644 --- a/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts +++ b/packages/cloudflare/src/instrumentations/instrumentSqlStorage.ts @@ -1,21 +1,14 @@ import type { SqlStorage } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; -import { - getActiveSpan, - getClient, - hasSpansEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanIsSampled, - startSpan, -} from '@sentry/core'; +import { getActiveSpan, getClient, hasSpansEnabled, spanIsSampled, startSpan } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; import type { CloudflareClientOptions } from '../client'; import { mayTargetCloudflareInternalTable, targetsCloudflareInternalTable } from '../utils/internalSqlQuery'; const SPAN_ATTRIBUTES = { [SENTRY_OP]: DB_QUERY, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', }; diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts index d8410ce32a45..42b01428816c 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentD1.ts @@ -1,16 +1,9 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { D1Database, D1DatabaseSession, D1PreparedStatement, D1Response } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; import type { Span, SpanAttributes, StartSpanOptions } from '@sentry/core'; -import { - addBreadcrumb, - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, -} from '@sentry/core'; +import { addBreadcrumb, getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; import { getSqlQuerySummary, sanitizeSqlQuery } from '@sentry/server-utils'; import { ensureInstrumented } from '../../instrument'; @@ -148,7 +141,7 @@ function createStartSpanOptions(queryText: string | undefined, type: D1QueryType 'db.operation.name': type, 'db.query.text': queryText, 'db.query.summary': querySummary, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', }, }; } @@ -187,7 +180,7 @@ function instrumentBatch( 'db.operation.name': 'batch', 'db.query.text': queryText || undefined, 'db.operation.batch.size': statements.length, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', }, }, async () => { diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts index 285be9fc1313..b12f01474d6a 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueue.ts @@ -1,9 +1,9 @@ import type { MessageBatch } from '@cloudflare/workers-types'; import type { AnyExportedHandler } from '../../types'; import type { env as cloudflareEnv, WorkerEntrypoint } from 'cloudflare:workers'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS } from '@sentry/conventions/op'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, withIsolationScope } from '@sentry/core'; +import { captureException, startSpan, withIsolationScope } from '@sentry/core'; import type { CloudflareOptions } from '../../client'; import { flushAndDispose } from '../../flush'; import { ensureInstrumented } from '../../instrument'; @@ -45,7 +45,7 @@ function wrapQueueHandler( 'messaging.operation.name': 'process', 'messaging.batch.message_count': batch.messages.length, 'messaging.message.retry.count': batch.messages.reduce((acc, message) => acc + message.attempts - 1, 0), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.queue', + [SENTRY_ORIGIN]: 'auto.faas.cloudflare.queue', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts index d1b3de4ba107..3460ea32c633 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentQueueProducer.ts @@ -1,7 +1,7 @@ import type { MessageSendRequest, Queue, QueueSendBatchOptions, QueueSendOptions } from '@cloudflare/workers-types'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { QUEUE_PUBLISH } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { startSpan } from '@sentry/core'; const ORIGIN = 'auto.faas.cloudflare.queue'; @@ -26,7 +26,7 @@ function startPublishSpan( ...(messageCount !== undefined && { 'messaging.batch.message_count': messageCount }), 'messaging.message.body.size': bodySize, [SENTRY_OP]: QUEUE_PUBLISH, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, }, callback, diff --git a/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts b/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts index 7d6ad24ae3e2..a2ffdf72e50a 100644 --- a/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts +++ b/packages/cloudflare/src/instrumentations/worker/instrumentR2.ts @@ -7,6 +7,7 @@ import { CLOUDFLARE_R2_REQUEST_PART_NUMBER, CLOUDFLARE_R2_REQUEST_PREFIX, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { OBJECT_DELETE, @@ -19,7 +20,7 @@ import { OBJECT_PUT, OBJECT_UPLOAD_PART, } from '@sentry/conventions/op'; -import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core'; +import { isObjectLike, startSpan } from '@sentry/core'; const ORIGIN = 'auto.faas.cloudflare.r2'; @@ -67,7 +68,7 @@ function createSpanOptions(bindingName: string, r2Op: R2OperationKey, key?: stri ...(isR2ListOptions(key) && key.prefix !== undefined && { [CLOUDFLARE_R2_REQUEST_PREFIX]: key.prefix }), ...(isR2ListOptions(key) && key.delimiter !== undefined && { [CLOUDFLARE_R2_REQUEST_DELIMITER]: key.delimiter }), [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, }; } diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 62442cadf32d..6043354e6427 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -1,15 +1,12 @@ -import { SENTRY_SEGMENT_NAME_SOURCE, CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + SENTRY_SEGMENT_NAME_SOURCE, + CODE_FUNCTION_NAME, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; import type { PropagationContext, Scope } from '@sentry/core'; -import { - captureException, - flush, - getCurrentScope, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - withIsolationScope, - withScope, -} from '@sentry/core'; +import { captureException, flush, getCurrentScope, startSpan, withIsolationScope, withScope } from '@sentry/core'; import type { WorkflowDelayDuration, WorkflowEntrypoint, @@ -146,7 +143,7 @@ class WrappedWorkflowStep implements WorkflowStep { typeof config?.retries?.delay === 'function' ? undefined : config?.retries?.delay, 'cloudflare.workflow.retries.limit': config?.retries?.limit, 'cloudflare.workflow.attempt': attempt, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow', + [SENTRY_ORIGIN]: 'auto.faas.cloudflare.workflow', [SENTRY_SEGMENT_NAME_SOURCE]: 'task', }, }, diff --git a/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts b/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts index af5161e5bcde..8a17c0031aac 100644 --- a/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts +++ b/packages/cloudflare/test/instrumentDurableObjectStorage.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as sentryCore from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { instrumentDurableObjectStorage } from '../src/instrumentations/instrumentDurableObjectStorage'; @@ -32,7 +32,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'get', }, @@ -53,7 +53,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'get', }, @@ -76,7 +76,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_put', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'put', }, @@ -97,7 +97,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_put', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'put', }, @@ -120,7 +120,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_delete', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'delete', }, @@ -141,7 +141,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_delete', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'delete', }, @@ -164,7 +164,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_list', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'list', }, @@ -187,7 +187,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_setAlarm', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'setAlarm', }, @@ -271,7 +271,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_getAlarm', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'getAlarm', }, @@ -292,7 +292,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_deleteAlarm', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare.durable_object.storage', 'db.operation.name': 'deleteAlarm', }, @@ -315,7 +315,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'SELECT', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'SELECT ?', @@ -439,7 +439,7 @@ describe('instrumentDurableObjectStorage', () => { name: 'durable_object_storage_kv_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'get', }, diff --git a/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts b/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts index f6135c2f919c..b75cc0e0e245 100644 --- a/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts +++ b/packages/cloudflare/test/instrumentDurableObjectSyncKvStorage.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as sentryCore from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { instrumentDurableObjectSyncKvStorage } from '../src/instrumentations/instrumentDurableObjectSyncKvStorage'; @@ -21,7 +21,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_get', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'get', }, @@ -64,7 +64,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_put', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'put', }, @@ -96,7 +96,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_delete', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'delete', }, @@ -129,7 +129,7 @@ describe('instrumentDurableObjectSyncKvStorage', () => { name: 'durable_object_storage_kv_list', attributes: { 'sentry.op': 'db', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'list', }, diff --git a/packages/cloudflare/test/instrumentSqlStorage.test.ts b/packages/cloudflare/test/instrumentSqlStorage.test.ts index 8d76316460f3..edf590f84a0f 100644 --- a/packages/cloudflare/test/instrumentSqlStorage.test.ts +++ b/packages/cloudflare/test/instrumentSqlStorage.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as sentryCore from '@sentry/core'; import * as serverUtils from '@sentry/server-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -27,7 +27,7 @@ describe('instrumentSqlStorage', () => { name: 'SELECT users', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'SELECT * FROM users WHERE id = ?', @@ -51,7 +51,7 @@ describe('instrumentSqlStorage', () => { name: 'SELECT users', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'SELECT * FROM users WHERE name = ? AND age > ?', @@ -84,7 +84,7 @@ describe('instrumentSqlStorage', () => { name: 'INSERT users', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', 'db.query.text': 'INSERT INTO users (name, email) VALUES (?, ?)', @@ -281,7 +281,7 @@ describe('instrumentSqlStorage', () => { name: 'exec', attributes: { 'sentry.op': 'db.query', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', + [SENTRY_ORIGIN]: 'auto.db.cloudflare.durable_object.sql', 'db.system.name': 'cloudflare-durable-object-sql', 'db.operation.name': 'exec', }, diff --git a/packages/deno/src/opentelemetry/tracer.ts b/packages/deno/src/opentelemetry/tracer.ts index 795f24eb9be4..e9846b994e45 100644 --- a/packages/deno/src/opentelemetry/tracer.ts +++ b/packages/deno/src/opentelemetry/tracer.ts @@ -1,6 +1,7 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, startSpanManual } from '@sentry/core'; +import { startInactiveSpan, startSpanManual } from '@sentry/core'; /** * Set up a mock OTEL tracer to allow inter-op with OpenTelemetry emitted spans. @@ -34,7 +35,7 @@ class SentryDenoTracer implements Tracer { name, attributes: { ...options?.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', 'sentry.deno_tracer': true, }, }); @@ -64,7 +65,7 @@ class SentryDenoTracer implements Tracer { name, attributes: { ...opts.attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + [SENTRY_ORIGIN]: 'manual', 'sentry.deno_tracer': true, }, }; diff --git a/packages/elysia/src/withElysia.ts b/packages/elysia/src/withElysia.ts index f423920872cc..ee8ac91f8cb5 100644 --- a/packages/elysia/src/withElysia.ts +++ b/packages/elysia/src/withElysia.ts @@ -5,6 +5,7 @@ import { SENTRY_SEGMENT_NAME_SOURCE, URL_FULL, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HANDLER, HTTP_SERVER, MIDDLEWARE } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; @@ -18,7 +19,6 @@ import { getTraceData, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, startInactiveSpan, startSpanManual, @@ -154,7 +154,7 @@ function instrumentLifecyclePhase( parentSpan: rootSpan, attributes: { [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, + [SENTRY_ORIGIN]: ELYSIA_ORIGIN, ...routeAttribute, }, }); @@ -169,7 +169,7 @@ function instrumentLifecyclePhase( parentSpan: phaseSpan, attributes: { [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, + [SENTRY_ORIGIN]: ELYSIA_ORIGIN, ...routeAttribute, // Streamed request handler spans are named after the route, so the // handler name has no other place to go. Anonymous handlers have no @@ -251,7 +251,7 @@ export function withElysia(app: T, options: ElysiaHandlerOp : `${request.method} ${new URL(request.url).pathname}`, attributes: { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ELYSIA_ORIGIN, + [SENTRY_ORIGIN]: ELYSIA_ORIGIN, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [URL_FULL]: filterCollectedUrl(request.url), [URL_PATH]: new URL(request.url).pathname, diff --git a/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts b/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts index e7b1f6c7e21b..5fae82f33b51 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts @@ -8,6 +8,7 @@ import { GCP_FUNCTION_CONTEXT_SOURCE, GCP_FUNCTION_CONTEXT_SPECVERSION, GCP_FUNCTION_CONTEXT_TIME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import { @@ -15,7 +16,6 @@ import { getClient, handleCallbackErrors, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK, } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; @@ -64,7 +64,7 @@ function _wrapCloudEventFunction( [FAAS_NAME]: functionName, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: context.type, [GCP_FUNCTION_CONTEXT_ID]: context.id, [GCP_FUNCTION_CONTEXT_SOURCE]: context.source, diff --git a/packages/google-cloud-serverless/src/gcpfunction/http.ts b/packages/google-cloud-serverless/src/gcpfunction/http.ts index a90cb8ad00fd..3775752497ef 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/http.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/http.ts @@ -5,6 +5,7 @@ import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import { @@ -14,7 +15,6 @@ import { hasSpanStreamingEnabled, httpRequestToRequestData, isString, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK, setHttpStatus, stripUrlQueryAndFragment, @@ -73,7 +73,7 @@ function _wrapHttpFunction(fn: HttpFunction, options: Partial): [FAAS_NAME]: functionName, [FAAS_TRIGGER]: 'http', [SENTRY_SEGMENT_NAME_SOURCE]: hasSpanStreaming ? 'component' : 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', // The method and path used to be the span name; they stay on the span so that // information survives the low-cardinality rename. [HTTP_REQUEST_METHOD]: reqMethod || undefined, diff --git a/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts b/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts index d41e1cb38ad7..e0cb9816f13f 100644 --- a/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts +++ b/packages/google-cloud-serverless/src/integrations/google-cloud-grpc.ts @@ -1,7 +1,7 @@ -import { RPC_METHOD, RPC_SERVICE, RPC_SYSTEM_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { RPC_METHOD, RPC_SERVICE, RPC_SYSTEM_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { GRPC } from '@sentry/conventions/op'; import type { Client, IntegrationFn } from '@sentry/core'; -import { defineIntegration, fill, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { defineIntegration, fill, getClient } from '@sentry/core'; import { startInactiveSpan } from '@sentry/node'; import type { EventEmitter } from 'events'; @@ -106,7 +106,7 @@ export function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodNa onlyIfParent: true, attributes: { [SENTRY_OP]: GRPC, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', + [SENTRY_ORIGIN]: 'auto.grpc.serverless', [RPC_SYSTEM_NAME]: 'grpc', [RPC_SERVICE]: serviceIdentifier, [RPC_METHOD]: methodName, diff --git a/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts b/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts index 91e99e031840..6359ed876333 100644 --- a/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts +++ b/packages/google-cloud-serverless/src/integrations/google-cloud-http.ts @@ -1,5 +1,12 @@ import type * as common from '@google-cloud/common'; -import { HTTP_REQUEST_METHOD, SENTRY_OP, SERVER_ADDRESS, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes'; +import { + HTTP_REQUEST_METHOD, + SENTRY_OP, + SERVER_ADDRESS, + URL_DOMAIN, + URL_FULL, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import type { Client, IntegrationFn } from '@sentry/core'; import { @@ -9,7 +16,6 @@ import { hasSpanStreamingEnabled, isURLObjectRelative, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, filterCollectedUrl, stripUrlQueryAndFragment, @@ -72,7 +78,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_CLIENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + [SENTRY_ORIGIN]: 'auto.http.serverless', [HTTP_REQUEST_METHOD]: httpMethod, [SERVER_ADDRESS]: serverAddress, [URL_DOMAIN]: serverAddress, diff --git a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts index 514a288f9531..a6bb52702d6a 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts @@ -8,11 +8,12 @@ import { GCP_FUNCTION_CONTEXT_SOURCE, GCP_FUNCTION_CONTEXT_SPECVERSION, GCP_FUNCTION_CONTEXT_TIME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { Client } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { wrapCloudEventFunction } from '../../src/gcpfunction/cloud_events'; import type { CloudEventFunction, CloudEventFunctionWithCallback } from '../../src/gcpfunction/general'; @@ -94,7 +95,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -126,7 +127,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -159,7 +160,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -203,7 +204,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -247,7 +248,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -276,7 +277,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', @@ -319,7 +320,7 @@ describe('wrapCloudEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'cloud_event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_cloud_event', [GCP_FUNCTION_CONTEXT_TYPE]: 'google.cloud.pubsub.topic.v1.messagePublished', [GCP_FUNCTION_CONTEXT_ID]: '5302804326013861', [GCP_FUNCTION_CONTEXT_SOURCE]: '//pubsub.googleapis.com/projects/my-project/topics/my-topic', diff --git a/packages/google-cloud-serverless/test/gcpfunction/events.test.ts b/packages/google-cloud-serverless/test/gcpfunction/events.test.ts index 95d1b6972924..c69251441b54 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/events.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/events.test.ts @@ -7,11 +7,12 @@ import { GCP_FUNCTION_CONTEXT_EVENT_ID, GCP_FUNCTION_CONTEXT_RESOURCE, GCP_FUNCTION_CONTEXT_TIMESTAMP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { Client, Event } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { wrapEventFunction } from '../../src/gcpfunction/events'; import type { EventFunction, EventFunctionWithCallback } from '../../src/gcpfunction/general'; @@ -88,7 +89,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -116,7 +117,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -149,7 +150,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -181,7 +182,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -211,7 +212,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -239,7 +240,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', @@ -268,7 +269,7 @@ describe('wrapEventFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'event', [SENTRY_SEGMENT_NAME_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_event', [GCP_FUNCTION_CONTEXT_EVENT_TYPE]: 'providers/cloud.firestore/eventTypes/document.write', [GCP_FUNCTION_CONTEXT_EVENT_ID]: '1144231683168617', [GCP_FUNCTION_CONTEXT_RESOURCE]: 'projects/my-project/databases/(default)/documents/users/abc123', diff --git a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts index 881fc6867bd6..a0704860c8bf 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts @@ -5,11 +5,12 @@ import { HTTP_REQUEST_METHOD, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { Client, Integration } from '@sentry/core'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, type MockInstance, test, vi } from 'vitest'; import type { HttpFunction, Request, Response } from '../../src/gcpfunction/general'; import { wrapHttpFunction } from '../../src/gcpfunction/http'; @@ -107,7 +108,7 @@ describe('GCPFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'http', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', [HTTP_REQUEST_METHOD]: 'POST', [URL_PATH]: '/path', }, @@ -134,7 +135,7 @@ describe('GCPFunction', () => { [FAAS_NAME]: undefined, [FAAS_TRIGGER]: 'http', [SENTRY_SEGMENT_NAME_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', + [SENTRY_ORIGIN]: 'auto.function.serverless.gcp_http', [HTTP_REQUEST_METHOD]: 'POST', [URL_PATH]: '/path', }, diff --git a/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts b/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts index 954f186a34d4..9d9aa0dcedf7 100644 --- a/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts +++ b/packages/google-cloud-serverless/test/integrations/google-cloud-grpc.test.ts @@ -1,5 +1,5 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; -import { createTransport, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setCurrentClient } from '@sentry/core'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { createTransport, setCurrentClient } from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { GrpcFunction, GrpcFunctionObject, Stub } from '../../src/integrations/google-cloud-grpc'; @@ -145,7 +145,7 @@ describe('GoogleCloudGrpc tracing', () => { onlyIfParent: true, attributes: { [SENTRY_OP]: 'grpc', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', + [SENTRY_ORIGIN]: 'auto.grpc.serverless', 'rpc.system.name': 'grpc', 'rpc.service': 'test-service', 'rpc.method': 'unaryMethod', diff --git a/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts b/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts index 3a0859914daf..a7d550eea515 100644 --- a/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts +++ b/packages/google-cloud-serverless/test/integrations/google-cloud-http.test.ts @@ -1,7 +1,13 @@ import { BigQuery } from '@google-cloud/bigquery'; -import { HTTP_REQUEST_METHOD, SENTRY_OP, SERVER_ADDRESS, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes'; +import { + HTTP_REQUEST_METHOD, + SENTRY_OP, + SERVER_ADDRESS, + URL_DOMAIN, + URL_FULL, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { createTransport, NodeClient, setCurrentClient } from '@sentry/node'; import * as fs from 'fs'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -93,7 +99,7 @@ describe('GoogleCloudHttp tracing', () => { onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_CLIENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + [SENTRY_ORIGIN]: 'auto.http.serverless', [HTTP_REQUEST_METHOD]: 'POST', [SERVER_ADDRESS]: 'bigquery.googleapis.com', [URL_DOMAIN]: 'bigquery.googleapis.com', @@ -105,7 +111,7 @@ describe('GoogleCloudHttp tracing', () => { onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_CLIENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', + [SENTRY_ORIGIN]: 'auto.http.serverless', [HTTP_REQUEST_METHOD]: 'GET', [SERVER_ADDRESS]: 'bigquery.googleapis.com', [URL_DOMAIN]: 'bigquery.googleapis.com', diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index a7e41bacc1b2..4d5762481d2f 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -1,6 +1,6 @@ import type { MonitorConfig } from '@sentry/core'; -import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; -import { captureException, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { CODE_FUNCTION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { captureException } from '@sentry/core'; import * as Sentry from '@sentry/node'; import { startSpan } from '@sentry/node'; import { isExpectedError } from './helpers'; @@ -42,7 +42,7 @@ export function SentryTraced(op: string = 'function') { op: op, name: propertyKey, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', [SENTRY_OP]: op, [CODE_FUNCTION_NAME]: propertyKey, }, diff --git a/packages/nestjs/src/integrations/helpers.ts b/packages/nestjs/src/integrations/helpers.ts index 8759a78b31ee..5056413e911a 100644 --- a/packages/nestjs/src/integrations/helpers.ts +++ b/packages/nestjs/src/integrations/helpers.ts @@ -5,16 +5,11 @@ import { MESSAGING_SYSTEM, SENTRY_DESCRIPTION, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, MIDDLEWARE, QUEUE_PROCESS } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; -import { - addNonEnumerableProperty, - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - withActiveSpan, -} from '@sentry/core'; +import { addNonEnumerableProperty, getClient, hasSpanStreamingEnabled, withActiveSpan } from '@sentry/core'; import type { CatchTarget, InjectableTarget, NextFunction, Observable, Subscription } from './types'; /** A function of unknown signature, matching the methods/handlers we wrap. */ @@ -100,7 +95,7 @@ export function getMiddlewareSpanOptions( name: name ?? target.name ?? 'unknown', attributes: { [SENTRY_OP]: MIDDLEWARE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: middlewareOrigin(componentType), + [SENTRY_ORIGIN]: middlewareOrigin(componentType), }, }; } @@ -124,7 +119,7 @@ export function getEventSpanOptions(event: string): { attributes: { [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: event, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs', + [SENTRY_ORIGIN]: 'auto.event.nestjs', // Relay infers a `function` span's description from `code.function.name` alone, which drops the prefix. ...(isStreamed && { [SENTRY_DESCRIPTION]: description }), }, @@ -157,7 +152,7 @@ export function getBullMQProcessSpanOptions(queueName: string | undefined): { name, attributes: { [SENTRY_OP]: QUEUE_PROCESS, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.queue.nestjs.bullmq', + [SENTRY_ORIGIN]: 'auto.queue.nestjs.bullmq', [MESSAGING_SYSTEM]: 'bullmq', [MESSAGING_OPERATION_TYPE]: PROCESS_OPERATION, [MESSAGING_DESTINATION_NAME]: queueName, diff --git a/packages/nestjs/src/integrations/wrap-route.ts b/packages/nestjs/src/integrations/wrap-route.ts index c65b6e2b69ce..406591804d49 100644 --- a/packages/nestjs/src/integrations/wrap-route.ts +++ b/packages/nestjs/src/integrations/wrap-route.ts @@ -4,6 +4,7 @@ import { HTTP_ROUTE, SENTRY_OP, URL_FULL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION, HANDLER } from '@sentry/conventions/op'; import type { SpanAttributes } from '@sentry/core'; @@ -11,7 +12,6 @@ import { getClient, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, filterCollectedUrl, } from '@sentry/core'; @@ -47,7 +47,7 @@ export function getAppCreationSpanOptions( attributes: { component: NESTJS_COMPONENT, [SENTRY_OP]: FUNCTION, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_ORIGIN]: HTTP_ORIGIN, [AttributeNames.TYPE]: NestType.APP_CREATION, [AttributeNames.VERSION]: moduleVersion || undefined, [AttributeNames.MODULE]: moduleName || undefined, @@ -68,7 +68,7 @@ export function wrapRouteHandler(callback: AnyFn, moduleVersion?: string): AnyFn const attributes: SpanAttributes = { component: NESTJS_COMPONENT, [SENTRY_OP]: HANDLER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_ORIGIN]: HTTP_ORIGIN, [AttributeNames.TYPE]: NestType.REQUEST_HANDLER, [AttributeNames.CALLBACK]: callback.name, [AttributeNames.VERSION]: moduleVersion || undefined, @@ -111,7 +111,7 @@ export function wrapRequestContextHandler( const httpRoute = req.route?.path || req.routeOptions?.url || req.routerPath; const attributes: SpanAttributes = { component: NESTJS_COMPONENT, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: HTTP_ORIGIN, + [SENTRY_ORIGIN]: HTTP_ORIGIN, [SENTRY_OP]: FUNCTION, [CODE_FUNCTION_NAME]: spanName, [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, diff --git a/packages/nestjs/test/decorators.test.ts b/packages/nestjs/test/decorators.test.ts index 8e6c083057ff..661f00b89182 100644 --- a/packages/nestjs/test/decorators.test.ts +++ b/packages/nestjs/test/decorators.test.ts @@ -1,7 +1,6 @@ import 'reflect-metadata'; -import { CODE_FUNCTION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as core from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { SentryCron, SentryExceptionCaptured, SentryTraced } from '../src/decorators'; import * as helpers from '../src/helpers'; @@ -41,7 +40,7 @@ describe('SentryTraced decorator', () => { op: 'test-operation', name: 'testMethod', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', [SENTRY_OP]: 'test-operation', [CODE_FUNCTION_NAME]: 'testMethod', }, @@ -75,7 +74,7 @@ describe('SentryTraced decorator', () => { op: 'function', // default value name: 'testDefaultOp', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', [SENTRY_OP]: 'function', [CODE_FUNCTION_NAME]: 'testDefaultOp', }, @@ -109,7 +108,7 @@ describe('SentryTraced decorator', () => { op: 'sync-operation', name: 'syncMethod', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', + [SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced', [SENTRY_OP]: 'sync-operation', [CODE_FUNCTION_NAME]: 'syncMethod', }, diff --git a/packages/nestjs/test/integrations/span-origin.test.ts b/packages/nestjs/test/integrations/span-origin.test.ts index 8eba4fb68e18..b1d0f3bc551f 100644 --- a/packages/nestjs/test/integrations/span-origin.test.ts +++ b/packages/nestjs/test/integrations/span-origin.test.ts @@ -1,4 +1,4 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { describe, expect, it } from 'vitest'; import { getBullMQProcessSpanOptions, @@ -8,15 +8,15 @@ import { } from '../../src/integrations/helpers'; function middlewareOrigin(componentType?: string): unknown { - return getMiddlewareSpanOptions({ name: 'X' }, undefined, componentType).attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + return getMiddlewareSpanOptions({ name: 'X' }, undefined, componentType).attributes[SENTRY_ORIGIN]; } function eventOrigin(): unknown { - return getEventSpanOptions('x').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + return getEventSpanOptions('x').attributes[SENTRY_ORIGIN]; } function bullmqOrigin(): unknown { - return getBullMQProcessSpanOptions('q').attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + return getBullMQProcessSpanOptions('q').attributes[SENTRY_ORIGIN]; } describe('NestJS span origin', () => { diff --git a/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts index 23660cc7f7ff..d341a3d9ffa3 100644 --- a/packages/node/src/integrations/fs/vendored/instrumentation.ts +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -14,11 +14,10 @@ * `error.type` (the syscall error code, e.g. `ENOENT`). */ -import { ERROR_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { ERROR_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { FILE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, getActiveSpan, startInactiveSpan, @@ -101,7 +100,7 @@ function getSpanAttributes( ): SpanAttributes { const attributes: SpanAttributes = { [SENTRY_OP]: SPAN_OP, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN, + [SENTRY_ORIGIN]: SPAN_ORIGIN, }; if (!config.recordFilePaths) { diff --git a/packages/node/src/integrations/http/httpServerSpansIntegration.ts b/packages/node/src/integrations/http/httpServerSpansIntegration.ts index 4712bdd61e7a..b690854c87d2 100644 --- a/packages/node/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node/src/integrations/http/httpServerSpansIntegration.ts @@ -24,6 +24,7 @@ import { URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import type { Event, Integration, IntegrationFn, Span, SpanAttributes, SpanStatus } from '@sentry/core'; @@ -39,7 +40,6 @@ import { httpHeadersToSpanAttributes, getContentLengthFromHeaders, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, stripUrlQueryAndFragment, isTracingSuppressed, @@ -168,7 +168,7 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions [SENTRY_KIND]: 'server', [SENTRY_OP]: HTTP_SERVER, [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.http_server', + [SENTRY_ORIGIN]: 'auto.http.http_server', [SENTRY_HTTP_PREFETCH]: isKnownPrefetchRequest(request) || undefined, [URL_FULL]: filterCollectedUrl(fullUrl, client), [URL_PATH]: urlObj?.pathname ?? httpTargetWithoutQueryFragment, diff --git a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts index 0def1c3be01d..4b9f9f82324c 100644 --- a/packages/node/src/integrations/node-fetch/undici-instrumentation.ts +++ b/packages/node/src/integrations/node-fetch/undici-instrumentation.ts @@ -31,7 +31,6 @@ import { parseUrl, safeCallback, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, stripDataUrlContent, @@ -59,6 +58,7 @@ import { URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_CLIENT } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../debug-build'; @@ -220,7 +220,7 @@ function onRequestCreated(config: NodeFetchOptions, { request }: RequestMessage) [URL_QUERY]: filterCollectedUrlQuery(getUrlQuery(requestUrl.search)), [URL_FRAGMENT]: getUrlFragment(requestUrl.hash), [URL_SCHEME]: urlScheme, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node_fetch', + [SENTRY_ORIGIN]: 'auto.http.node_fetch', }; // Sanitize data URLs to prevent long base64 strings in span attributes diff --git a/packages/node/src/utils/addOriginToSpan.ts b/packages/node/src/utils/addOriginToSpan.ts index 2a23710fa7cf..afb1c8872aaf 100644 --- a/packages/node/src/utils/addOriginToSpan.ts +++ b/packages/node/src/utils/addOriginToSpan.ts @@ -1,8 +1,8 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Span } from '@opentelemetry/api'; import type { SpanOrigin } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; /** Adds an origin to an OTEL Span. */ export function addOriginToSpan(span: Span, origin: SpanOrigin): void { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, origin); + span.setAttribute(SENTRY_ORIGIN, origin); } diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index a4bf5e3281ff..681c988eef10 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -1,7 +1,6 @@ -import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { ErrorEvent, TransactionEvent } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Sentry from '../../src'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -38,7 +37,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, span => { @@ -168,7 +167,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, span => { @@ -460,7 +459,7 @@ describe('Integration | Transactions', () => { name: 'test name', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'task', - [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + [SENTRY_ORIGIN]: 'auto.test', }, }, () => { diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index f278ccc726d7..807b16ac5468 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -10,8 +10,8 @@ import { URL_PATH, URL_QUERY, URL_SCHEME, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { channel } from 'node:diagnostics_channel'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import type { UndiciRequest } from '../../src/integrations/node-fetch/types'; @@ -70,7 +70,7 @@ describe('instrumentUndici', () => { [URL_QUERY]: 'limit=10', [URL_FRAGMENT]: undefined, [URL_SCHEME]: 'https', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node_fetch', + [SENTRY_ORIGIN]: 'auto.http.node_fetch', [SERVER_ADDRESS]: 'api.example.com', [SERVER_PORT]: 443, }, diff --git a/packages/server-utils/src/ai/anthropic-ai/index.ts b/packages/server-utils/src/ai/anthropic-ai/index.ts index f45d875d8b6e..c285a043bc4c 100644 --- a/packages/server-utils/src/ai/anthropic-ai/index.ts +++ b/packages/server-utils/src/ai/anthropic-ai/index.ts @@ -1,12 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, - startSpanManual, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startSpan, startSpanManual } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_OPERATION_NAME, @@ -22,6 +15,7 @@ import { GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_TOOL_DEFINITIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; @@ -56,7 +50,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: 'anthropic', [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', + [SENTRY_ORIGIN]: 'auto.ai.anthropic', }; if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { diff --git a/packages/server-utils/src/ai/flue/index.ts b/packages/server-utils/src/ai/flue/index.ts index 67e59c9fcdc6..7243aeb1351f 100644 --- a/packages/server-utils/src/ai/flue/index.ts +++ b/packages/server-utils/src/ai/flue/index.ts @@ -5,11 +5,15 @@ import { continueTrace, getActiveSpan, LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, withActiveSpan, } from '@sentry/core'; -import { GEN_AI_AGENT_NAME, GEN_AI_CONVERSATION_ID, GEN_AI_OPERATION_NAME } from '@sentry/conventions/attributes'; +import { + GEN_AI_AGENT_NAME, + GEN_AI_CONVERSATION_ID, + GEN_AI_OPERATION_NAME, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants'; import type { GenAiOptions } from '../core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; @@ -104,7 +108,7 @@ export function createFlueInstrumentation(options: FlueOptions = {}): FlueInstru name: `invoke_agent ${ctx.agentName ?? 'agent'}`, op: getGenAiSpanOp('invoke_agent'), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FLUE_ORIGIN, + [SENTRY_ORIGIN]: FLUE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'invoke_agent', ...(ctx.agentName ? { [GEN_AI_AGENT_NAME]: ctx.agentName } : {}), ...(ctx.conversationId ? { [GEN_AI_CONVERSATION_ID]: ctx.conversationId } : {}), diff --git a/packages/server-utils/src/ai/flue/utils.ts b/packages/server-utils/src/ai/flue/utils.ts index a1b463cbfbd7..2661eea0892c 100644 --- a/packages/server-utils/src/ai/flue/utils.ts +++ b/packages/server-utils/src/ai/flue/utils.ts @@ -1,12 +1,5 @@ import type { LRUMap, Span } from '@sentry/core'; -import { - captureException, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, - stringify, - withActiveSpan, -} from '@sentry/core'; +import { captureException, SPAN_STATUS_ERROR, startInactiveSpan, stringify, withActiveSpan } from '@sentry/core'; import { GEN_AI_CONVERSATION_ID, GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, @@ -37,6 +30,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getGenAiSpanOp } from '../core/utils'; import { FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants'; @@ -123,7 +117,7 @@ export function startTurnSpan(observation: FlueObservation, turnSpans: SpanTrack name: 'chat', op: getGenAiSpanOp('chat'), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FLUE_ORIGIN, + [SENTRY_ORIGIN]: FLUE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'chat', ...(observation.conversationId ? { [GEN_AI_CONVERSATION_ID]: observation.conversationId } : {}), // No conventional attribute for this; it is the only way to tell a compaction turn from a @@ -237,7 +231,7 @@ export function startToolSpan(observation: FlueObservation, toolSpans: SpanTrack name: `execute_tool ${toolName ?? 'unknown'}`, op: getGenAiSpanOp('execute_tool'), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FLUE_ORIGIN, + [SENTRY_ORIGIN]: FLUE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'execute_tool', ...(toolName ? { [GEN_AI_TOOL_NAME]: toolName } : {}), ...(observation.conversationId ? { [GEN_AI_CONVERSATION_ID]: observation.conversationId } : {}), diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index 01b7cfb0f6c0..06bd425bc188 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -4,7 +4,6 @@ import { getClient, handleCallbackErrors, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, startSpanManual, @@ -31,6 +30,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { buildMethodPath, getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; @@ -113,7 +113,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: GOOGLE_GENAI_SYSTEM_NAME, [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [SENTRY_ORIGIN]: 'auto.ai.google_genai', }; if (params) { diff --git a/packages/server-utils/src/ai/langchain/embeddings.ts b/packages/server-utils/src/ai/langchain/embeddings.ts index 1a8f61c867f6..e1c4fd25fd9b 100644 --- a/packages/server-utils/src/ai/langchain/embeddings.ts +++ b/packages/server-utils/src/ai/langchain/embeddings.ts @@ -1,10 +1,4 @@ -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - stringify, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, startSpan, stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_EMBEDDINGS_INPUT, @@ -12,6 +6,7 @@ import { GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; import { @@ -45,7 +40,7 @@ function extractEmbeddingAttributes(instance: unknown): Record const embeddingsInstance = (instance ?? {}) as Record; const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, + [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, [SENTRY_OP]: GEN_AI_EMBEDDINGS, [GEN_AI_OPERATION_NAME]: 'embeddings', [GEN_AI_REQUEST_MODEL]: embeddingsInstance.model ?? 'unknown', diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index ea60eef3305e..e9d7df2a9d3d 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -1,12 +1,5 @@ /* eslint-disable max-lines */ -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpanManual, - stringify, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startSpanManual, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_OPERATION_NAME, @@ -17,6 +10,7 @@ import { GEN_AI_TOOL_DEFINITIONS, GEN_AI_TOOL_NAME, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_CHAT, GEN_AI_EXECUTE_TOOL, GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { resolveAIRecordingOptions } from '../core/utils'; @@ -227,7 +221,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const chainName = runName || chain.name; const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', + [SENTRY_ORIGIN]: 'auto.ai.langchain', [GEN_AI_OPERATION_NAME]: 'invoke_agent', }; @@ -307,7 +301,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const toolName = runName || tool.name || 'unknown_tool'; const attributes: Record = { ...getAgentNameFromMetadata(metadata), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, + [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, [GEN_AI_OPERATION_NAME]: 'execute_tool', [GEN_AI_TOOL_NAME]: toolName, }; diff --git a/packages/server-utils/src/ai/langchain/utils.ts b/packages/server-utils/src/ai/langchain/utils.ts index 4c29d93068ec..fa5a4948d519 100644 --- a/packages/server-utils/src/ai/langchain/utils.ts +++ b/packages/server-utils/src/ai/langchain/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ /* eslint-disable typescript-eslint/no-deprecated */ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, stringify } from '@sentry/core'; +import { stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_AGENT_NAME, @@ -24,6 +24,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE, GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../core/gen-ai-attributes'; import { extractSystemInstructions } from '../core/utils'; @@ -227,7 +228,7 @@ function baseRequestAttributes( [GEN_AI_PROVIDER_NAME]: stringify(system ?? 'langchain', String), [GEN_AI_OPERATION_NAME]: 'chat', [GEN_AI_REQUEST_MODEL]: stringify(modelName, String), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, + [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, ...extractCommonRequestAttributes(serialized, invocationParams, langSmithMetadata), }; } diff --git a/packages/server-utils/src/ai/langgraph/index.ts b/packages/server-utils/src/ai/langgraph/index.ts index 00825cbd6206..6795c3bd6317 100644 --- a/packages/server-utils/src/ai/langgraph/index.ts +++ b/packages/server-utils/src/ai/langgraph/index.ts @@ -1,5 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, stringify } from '@sentry/core'; +import { SPAN_STATUS_ERROR, startSpan, stringify } from '@sentry/core'; import { GEN_AI_AGENT_NAME, GEN_AI_CONVERSATION_ID, @@ -10,6 +10,7 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_INVOKE_AGENT } from '@sentry/conventions/op'; import { extractSystemInstructions, resolveAIRecordingOptions } from '../core/utils'; @@ -99,7 +100,7 @@ export function instrumentCompiledGraphInvoke( { name: 'invoke_agent', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, + [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, [SENTRY_OP]: GEN_AI_INVOKE_AGENT, [GEN_AI_OPERATION_NAME]: 'invoke_agent', }, diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index 86e43d2d33a0..94e39318d10e 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; +import { SPAN_STATUS_ERROR, startSpan } from '@sentry/core'; import type { Span, SpanAttributes } from '@sentry/core'; import { GEN_AI_AGENT_NAME, @@ -16,6 +16,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../core/gen-ai-attributes'; @@ -75,7 +76,7 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions, const wrappedInvoke = new Proxy(originalInvoke as (...args: unknown[]) => unknown, { apply(target, thisArg, args: unknown[]): unknown { const spanAttributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, + [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, [SENTRY_OP]: GEN_AI_EXECUTE_TOOL, [GEN_AI_OPERATION_NAME]: 'execute_tool', [GEN_AI_TOOL_NAME]: toolName, diff --git a/packages/server-utils/src/ai/mastra/index.ts b/packages/server-utils/src/ai/mastra/index.ts index 48db51f77fcc..0bfcdd712777 100644 --- a/packages/server-utils/src/ai/mastra/index.ts +++ b/packages/server-utils/src/ai/mastra/index.ts @@ -5,11 +5,10 @@ import { getActiveSpan, getClient, LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; -import { GEN_AI_RESPONSE_MODEL, SENTRY_OP } from '@sentry/conventions/attributes'; +import { GEN_AI_RESPONSE_MODEL, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../../debug-build'; import type { GenAiOptions } from '../core/utils'; import { resolveAIRecordingOptions } from '../core/utils'; @@ -144,7 +143,7 @@ export class SentryMastraExporter implements MastraObservabilityExporter { attributes: { ...this._attributesFor(span), [SENTRY_OP]: getOperation(span.type)?.op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MASTRA_ORIGIN, + [SENTRY_ORIGIN]: MASTRA_ORIGIN, }, }); diff --git a/packages/server-utils/src/ai/mistral/index.ts b/packages/server-utils/src/ai/mistral/index.ts index 96a335d0ebf2..4f6ba7b25f7e 100644 --- a/packages/server-utils/src/ai/mistral/index.ts +++ b/packages/server-utils/src/ai/mistral/index.ts @@ -1,10 +1,4 @@ -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startSpan, - startSpanManual, - stringify, -} from '@sentry/core'; +import { SPAN_STATUS_ERROR, startSpan, startSpanManual, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_AGENT_NAME, @@ -14,6 +8,7 @@ import { GEN_AI_PROVIDER_NAME, GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { @@ -55,7 +50,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: MISTRAL_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MISTRAL_ORIGIN, + [SENTRY_ORIGIN]: MISTRAL_ORIGIN, [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: streaming, }; diff --git a/packages/server-utils/src/ai/openai/index.ts b/packages/server-utils/src/ai/openai/index.ts index 7bed7ab27743..981da4281ef5 100644 --- a/packages/server-utils/src/ai/openai/index.ts +++ b/packages/server-utils/src/ai/openai/index.ts @@ -3,7 +3,6 @@ import { DEBUG_BUILD } from '../../debug-build'; import { getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startSpan, startSpanManual, @@ -19,6 +18,7 @@ import { GEN_AI_REQUEST_MODEL, GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { @@ -67,7 +67,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: 'openai', [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', + [SENTRY_ORIGIN]: 'auto.ai.openai', }; if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { diff --git a/packages/server-utils/src/ai/typesafe/index.ts b/packages/server-utils/src/ai/typesafe/index.ts index 328853cf5b02..0155969192d0 100644 --- a/packages/server-utils/src/ai/typesafe/index.ts +++ b/packages/server-utils/src/ai/typesafe/index.ts @@ -8,16 +8,10 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, - stringify, - withActiveSpan, -} from '@sentry/core'; +import { isObjectLike, SPAN_STATUS_ERROR, startInactiveSpan, stringify, withActiveSpan } from '@sentry/core'; import type { GenAiOptions } from '../core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; import { TYPESAFE_ORIGIN, TYPESAFE_PROVIDER_NAME } from './constants'; @@ -45,7 +39,7 @@ function getRequestAttributes( recordInputs: boolean, ): SpanAttributes { return { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: TYPESAFE_ORIGIN, + [SENTRY_ORIGIN]: TYPESAFE_ORIGIN, [GEN_AI_OPERATION_NAME]: 'evaluate', [GEN_AI_PROVIDER_NAME]: TYPESAFE_PROVIDER_NAME, ...(model ? { [GEN_AI_REQUEST_MODEL]: model } : {}), diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index 4af2c3424e05..b242e62f7014 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -14,9 +14,10 @@ import { GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_SYSTEM_INSTRUCTIONS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; -import { isObjectLike, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, stringify } from '@sentry/core'; +import { isObjectLike, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; import { @@ -69,7 +70,7 @@ export function extractRequestAttributes( const attributes: Record = { [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: operationName, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, + [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, [GEN_AI_REQUEST_MODEL]: typeof model === 'string' ? model : 'unknown', }; diff --git a/packages/server-utils/src/integrations/amqplib.ts b/packages/server-utils/src/integrations/amqplib.ts index b67b546d8b2f..9b227d7e0e2c 100644 --- a/packages/server-utils/src/integrations/amqplib.ts +++ b/packages/server-utils/src/integrations/amqplib.ts @@ -7,7 +7,6 @@ import { getClient, getTraceData, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, timestampInSeconds, @@ -26,6 +25,7 @@ import { SERVER_ADDRESS, SERVER_PORT, URL_FULL, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS, QUEUE_PUBLISH } from '@sentry/conventions/op'; import { amqplibModuleNames } from '../orchestrion/config/amqplib'; @@ -464,7 +464,7 @@ function startPublishSpan(data: AmqpChannelContext): Span { [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_VALUE_SEND, [MESSAGING_MESSAGE_ID]: options?.messageId as string | undefined, [ATTR_MESSAGING_CONVERSATION_ID]: options?.correlationId as string | undefined, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PUBLISHER_ORIGIN, + [SENTRY_ORIGIN]: PUBLISHER_ORIGIN, }, }); @@ -510,7 +510,7 @@ function startConsumeSpan(queue: string | undefined, msg: ConsumeMessage, channe [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_VALUE_PROCESS, [MESSAGING_MESSAGE_ID]: msg.properties?.messageId as string | undefined, [ATTR_MESSAGING_CONVERSATION_ID]: msg.properties?.correlationId as string | undefined, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: CONSUMER_ORIGIN, + [SENTRY_ORIGIN]: CONSUMER_ORIGIN, }, }); } diff --git a/packages/server-utils/src/integrations/anthropic.ts b/packages/server-utils/src/integrations/anthropic.ts index 33517d4730c2..d14d3cbb2f6a 100644 --- a/packages/server-utils/src/integrations/anthropic.ts +++ b/packages/server-utils/src/integrations/anthropic.ts @@ -1,4 +1,4 @@ -import { GEN_AI_REQUEST_MODEL } from '@sentry/conventions/attributes'; +import { GEN_AI_REQUEST_MODEL, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { @@ -7,7 +7,6 @@ import { getActiveSpan, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; @@ -115,7 +114,7 @@ function createGenAiSpan( const attributes = extractRequestAttributes(args, operation, recordInputs); const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; + attributes[SENTRY_ORIGIN] = ORIGIN; const client = getClient(); const span = startInactiveSpan({ diff --git a/packages/server-utils/src/integrations/aws-sdk/index.ts b/packages/server-utils/src/integrations/aws-sdk/index.ts index eb966dcd56c6..25a75931deb7 100644 --- a/packages/server-utils/src/integrations/aws-sdk/index.ts +++ b/packages/server-utils/src/integrations/aws-sdk/index.ts @@ -1,12 +1,13 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { defineIntegration, startInactiveSpan } from '@sentry/core'; import { _AWS_REQUEST_ID as AWS_REQUEST_ID, AWS_REQUEST_EXTENDED_ID, CLOUD_REGION, SENTRY_KIND, HTTP_RESPONSE_STATUS_CODE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { RPC } from '@sentry/conventions/op'; import { CHANNELS } from '../../orchestrion/channels'; @@ -111,7 +112,7 @@ function instrumentAwsSdk(servicesExtensions: ServicesExtensions): void { op: requestMetadata.spanOp || RPC, attributes: { [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: AWS_SDK_ORIGIN, + [SENTRY_ORIGIN]: AWS_SDK_ORIGIN, ...extractAttributesFromNormalizedRequest(normalizedRequest), ...requestMetadata.spanAttributes, }, diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index ac928f3ceeca..89decb7338e9 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -6,6 +6,7 @@ import { DB_OPERATION_NAME, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; import type { IntegrationFn, Span, StartSpanOptions } from '@sentry/core'; @@ -15,7 +16,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, startSpan, waitForTracingChannelBinding, @@ -113,7 +113,7 @@ function makeSpanOptions( // the vendored OTel instrumentation. The `batch` runs off a deferred tick with no obvious network // peer, so it gets no kind. [SENTRY_KIND]: operation === 'batch' ? undefined : 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_OPERATION_NAME]: operation, // A loader batches and caches one entity type, so it is the closest thing dataloader has to a // collection. Unnamed loaders report nothing. diff --git a/packages/server-utils/src/integrations/express/instrumentation.ts b/packages/server-utils/src/integrations/express/instrumentation.ts index ae442c454602..753ebbc90660 100644 --- a/packages/server-utils/src/integrations/express/instrumentation.ts +++ b/packages/server-utils/src/integrations/express/instrumentation.ts @@ -1,5 +1,5 @@ import type * as diagnosticsChannel from 'node:diagnostics_channel'; -import { HTTP_ROUTE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { HTTP_ROUTE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HANDLER, MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import { @@ -12,7 +12,6 @@ import { hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, stringMatchesSomePattern, withActiveSpan, @@ -340,7 +339,7 @@ function getSpanForLayer(data: HandleChannelContext, options: ExpressIntegration ? matchedRoute || REQUEST_HANDLER_SPAN_NAME_FALLBACK : name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: EXPRESS_TYPE_TO_SPAN_OP[type], [ATTR_EXPRESS_NAME]: name, [ATTR_EXPRESS_TYPE]: type, diff --git a/packages/server-utils/src/integrations/fastify/instrumentation.ts b/packages/server-utils/src/integrations/fastify/instrumentation.ts index 92f71c908fe4..6d34c4f66f2d 100644 --- a/packages/server-utils/src/integrations/fastify/instrumentation.ts +++ b/packages/server-utils/src/integrations/fastify/instrumentation.ts @@ -21,6 +21,7 @@ import { HTTP_ROUTE, SENTRY_OP, URL_PATH, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HANDLER, MIDDLEWARE } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; @@ -30,7 +31,6 @@ import { getIsolationScope, hasSpanStreamingEnabled, REQUEST_HANDLER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, startSpan, @@ -168,7 +168,7 @@ function onRequest(this: any, request: any, _reply: any, hookDone: () => void): getIsolationScope().setTransactionName(`${method} ${routeName}`); const attributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: HANDLER, [HTTP_REQUEST_METHOD]: request.method, [URL_PATH]: request.url, @@ -359,7 +359,7 @@ function handlerWrapper(handler: AnyFn, hookName: string, spanAttributes: Record attributes: { ...spanAttributes, [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, parentSpan, }, diff --git a/packages/server-utils/src/integrations/firebase/firestore.ts b/packages/server-utils/src/integrations/firebase/firestore.ts index 161daca6abaf..43928bd504b0 100644 --- a/packages/server-utils/src/integrations/firebase/firestore.ts +++ b/packages/server-utils/src/integrations/firebase/firestore.ts @@ -8,10 +8,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import type { FirebaseApp, FirebaseOptions, FirestoreReference, FirestoreSettings } from './firestore-types'; /** @@ -24,7 +25,7 @@ export function startFirestoreSpan(spanName: string, reference: FirestoreReferen attributes: { [SENTRY_OP]: DB_QUERY, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.firebase.firestore', + [SENTRY_ORIGIN]: 'auto.firebase.firestore', [DB_OPERATION_NAME]: spanName, ...buildAttributes(reference), }, diff --git a/packages/server-utils/src/integrations/firebase/functions.ts b/packages/server-utils/src/integrations/firebase/functions.ts index c8a919ef6fa8..75fd439a3cd6 100644 --- a/packages/server-utils/src/integrations/firebase/functions.ts +++ b/packages/server-utils/src/integrations/firebase/functions.ts @@ -5,6 +5,7 @@ import { SENTRY_KIND, SENTRY_OP, SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { FUNCTION_GCP } from '@sentry/conventions/op'; import type { SpanAttributes } from '@sentry/core'; @@ -13,7 +14,6 @@ import { flush, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SERVERLESS_FUNCTION_SPAN_NAME_FALLBACK, SPAN_STATUS_ERROR, startSpanManual, @@ -65,7 +65,7 @@ function wrapHandler(handler: Handler, triggerType: string): Handler { const enhancedTriggerType = `firebase.function.${triggerType}`; const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: FUNCTIONS_ORIGIN, + [SENTRY_ORIGIN]: FUNCTIONS_ORIGIN, [FAAS_NAME]: functionName, [FAAS_TRIGGER]: triggerType, 'faas.provider': 'firebase', diff --git a/packages/server-utils/src/integrations/generic-pool.ts b/packages/server-utils/src/integrations/generic-pool.ts index 40d0f4c530a1..f5236f5a364c 100644 --- a/packages/server-utils/src/integrations/generic-pool.ts +++ b/packages/server-utils/src/integrations/generic-pool.ts @@ -1,8 +1,8 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { defineIntegration, startInactiveSpan } from '@sentry/core'; import { CHANNELS } from '../orchestrion/channels'; import { genericPoolModuleNames } from '../orchestrion/config/generic-pool'; import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; @@ -41,7 +41,7 @@ function instrumentGenericPool(): void { name: 'generic-pool.acquire', attributes: { [SENTRY_OP]: DB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.generic_pool', + [SENTRY_ORIGIN]: 'auto.db.generic_pool', }, }), ); diff --git a/packages/server-utils/src/integrations/google-genai.ts b/packages/server-utils/src/integrations/google-genai.ts index 6ddaf8757d6a..b191c503df5b 100644 --- a/packages/server-utils/src/integrations/google-genai.ts +++ b/packages/server-utils/src/integrations/google-genai.ts @@ -7,7 +7,6 @@ import { getActiveSpan, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, startInactiveSpan, } from '@sentry/core'; @@ -116,7 +115,7 @@ function createGenAiSpan( const attributes = extractRequestAttributes(operation, params, data.self, recordInputs); const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown'; - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; + attributes[SENTRY_ORIGIN] = ORIGIN; const client = getClient(); const span = startInactiveSpan({ diff --git a/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts index 57aea8904715..8ea8a5d91944 100644 --- a/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/graphql/graphql-dc-subscriber.ts @@ -4,15 +4,10 @@ import { GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { GRAPHQL_FIELD_NAME, @@ -163,7 +158,7 @@ function setupParseChannel(tracingChannel: GraphqlTracingChannelFactory): void { return startInactiveSpan({ name: client && hasSpanStreamingEnabled(client) ? `GraphQL ${PROCESSING_TYPE_PARSE}` : SPAN_NAME_PARSE, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_PARSE, }, @@ -180,7 +175,7 @@ function setupValidateChannel(tracingChannel: GraphqlTracingChannelFactory): voi return startInactiveSpan({ name: client && hasSpanStreamingEnabled(client) ? `GraphQL ${PROCESSING_TYPE_VALIDATE}` : SPAN_NAME_VALIDATE, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_VALIDATE, [GRAPHQL_DOCUMENT]: collectGraphqlDocument(data.document), @@ -216,7 +211,7 @@ function setupOperationChannel( ? streamedName : getOperationSpanName(data.operationType, data.operationName, fallbackName), attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_EXECUTE, [GRAPHQL_OPERATION_TYPE]: data.operationType, @@ -258,7 +253,7 @@ function setupResolveChannel(tracingChannel: GraphqlTracingChannelFactory, ignor ? `GraphQL ${PROCESSING_TYPE_RESOLVE}` : `${SPAN_NAME_RESOLVE} ${data.fieldPath}`, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_RESOLVE, [GRAPHQL_FIELD_NAME]: data.fieldName, diff --git a/packages/server-utils/src/integrations/graphql/resolvers.ts b/packages/server-utils/src/integrations/graphql/resolvers.ts index d5c36ccd4915..c432b06b79ec 100644 --- a/packages/server-utils/src/integrations/graphql/resolvers.ts +++ b/packages/server-utils/src/integrations/graphql/resolvers.ts @@ -6,14 +6,13 @@ * call runs). Resolver spans use the same origin/op/field attributes as the native subscriber. */ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan, @@ -182,7 +181,7 @@ function createFieldIfNotExists( function createResolverSpan(info: GraphQLResolveInfo, path: string[], parentSpan?: Span): Span { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: GRAPHQL, [GRAPHQL_PROCESSING_TYPE]: PROCESSING_TYPE_RESOLVE, [GRAPHQL_FIELD_NAME]: info.fieldName, diff --git a/packages/server-utils/src/integrations/graphql/spans.ts b/packages/server-utils/src/integrations/graphql/spans.ts index fb0ef6a624b7..329407428a81 100644 --- a/packages/server-utils/src/integrations/graphql/spans.ts +++ b/packages/server-utils/src/integrations/graphql/spans.ts @@ -10,16 +10,11 @@ import { GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GRAPHQL } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; -import { - getClient, - hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SPAN_STATUS_ERROR, - startInactiveSpan, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, startInactiveSpan } from '@sentry/core'; import type { GraphqlDocumentNode } from './types'; import { collectGraphqlDocument, getOperationSpanName, hasResultErrors, renameRootSpanWithOperation } from './utils'; import { @@ -44,7 +39,7 @@ import type { } from './types'; const BASE_ATTRIBUTES = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: GRAPHQL, } as const; diff --git a/packages/server-utils/src/integrations/hapi/hapi-utils.ts b/packages/server-utils/src/integrations/hapi/hapi-utils.ts index 21c27d91eec5..5c7afa11a268 100644 --- a/packages/server-utils/src/integrations/hapi/hapi-utils.ts +++ b/packages/server-utils/src/integrations/hapi/hapi-utils.ts @@ -16,10 +16,9 @@ import { isObjectLike, REQUEST_HANDLER_SPAN_NAME_FALLBACK, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/core'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HANDLER, MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import type { HapiRequest, @@ -179,7 +178,7 @@ function startMetadataSpan(metadata: SpanMetadata, original: () => unknown): unk attributes: { ...metadata.attributes, [SENTRY_OP]: op, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.hapi', + [SENTRY_ORIGIN]: 'auto.http.hapi', }, }, original, diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index 6c30f31ccafb..d62e949125ef 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -1,9 +1,9 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import { createRequire } from 'node:module'; -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { debug, defineIntegration, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { Env, GetConnInfo, Hono, MiddlewareHandler } from './honoTypes'; import { CHANNELS } from '../../orchestrion/channels'; @@ -171,7 +171,7 @@ function instrumentInternalRequests(): void { name: `${method} ${extractPathname(input)}`, attributes: { [SENTRY_OP]: HTTP_SERVER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, + [SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, }, }); }, diff --git a/packages/server-utils/src/integrations/hono/patchAppRequest.ts b/packages/server-utils/src/integrations/hono/patchAppRequest.ts index bc54e3716b32..15bb20cc21fd 100644 --- a/packages/server-utils/src/integrations/hono/patchAppRequest.ts +++ b/packages/server-utils/src/integrations/hono/patchAppRequest.ts @@ -1,13 +1,6 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; -import { - debug, - getActiveSpan, - getOriginalFunction, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startSpan, - type WrappedFunction, -} from '@sentry/core'; +import { debug, getActiveSpan, getOriginalFunction, startSpan, type WrappedFunction } from '@sentry/core'; import { INTERNAL_REQUEST_ORIGIN } from './constants'; import type { Env, Hono } from './honoTypes'; import { DEBUG_BUILD } from '../../debug-build'; @@ -104,7 +97,7 @@ export function patchAppRequest(app: Hono): void { onlyIfParent: true, attributes: { [SENTRY_OP]: INTERNAL_REQUEST_OP, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, + [SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, }, }, () => { diff --git a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts index e3e451247614..86f96e05039c 100644 --- a/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts +++ b/packages/server-utils/src/integrations/hono/wrapMiddlewareSpan.ts @@ -1,10 +1,9 @@ -import { SENTRY_OP } from '@sentry/conventions/attributes'; +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import { getActiveSpan, getOriginalFunction, getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, type WrappedFunction, @@ -43,7 +42,7 @@ export function wrapMiddlewareWithSpan(handler: MiddlewareHandler): MiddlewareHa parentSpan: rootSpan, attributes: { [SENTRY_OP]: MIDDLEWARE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MIDDLEWARE_ORIGIN, + [SENTRY_ORIGIN]: MIDDLEWARE_ORIGIN, }, }); diff --git a/packages/server-utils/src/integrations/kafkajs/spans.ts b/packages/server-utils/src/integrations/kafkajs/spans.ts index 4aef66e08395..fdae5a0d46bd 100644 --- a/packages/server-utils/src/integrations/kafkajs/spans.ts +++ b/packages/server-utils/src/integrations/kafkajs/spans.ts @@ -18,6 +18,7 @@ import { MESSAGING_SYSTEM, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { QUEUE_PROCESS, QUEUE_PUBLISH, QUEUE_RECEIVE } from '@sentry/conventions/op'; import type { Span, SpanAttributes, SpanLink } from '@sentry/core'; @@ -26,7 +27,6 @@ import { getTraceData, hasSpanStreamingEnabled, propagationContextFromHeaders, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; @@ -135,7 +135,7 @@ export function startConsumerSpan({ topic, message, operationType, links, attrib [ATTR_MESSAGING_KAFKA_OFFSET]: message?.offset as string | undefined, // Mirror the upstream behavior of only tagging per-message processing spans (not the batch // receiving span, which carries no message) with the auto origin. - ...(message ? { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: CONSUMER_ORIGIN } : {}), + ...(message ? { [SENTRY_ORIGIN]: CONSUMER_ORIGIN } : {}), }, }); } @@ -155,7 +155,7 @@ export function startProducerSpan(topic: string, message: Message): Span { message.partition !== undefined ? String(message.partition) : undefined, [MESSAGING_OPERATION_NAME]: 'send', [MESSAGING_OPERATION_TYPE]: MESSAGING_OPERATION_TYPE_VALUE_SEND, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRODUCER_ORIGIN, + [SENTRY_ORIGIN]: PRODUCER_ORIGIN, }, }); diff --git a/packages/server-utils/src/integrations/knex.ts b/packages/server-utils/src/integrations/knex.ts index 5d85d1491682..781d26aa70c8 100644 --- a/packages/server-utils/src/integrations/knex.ts +++ b/packages/server-utils/src/integrations/knex.ts @@ -10,7 +10,6 @@ import { getActiveSpan, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, truncate, @@ -28,6 +27,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../debug-build'; @@ -182,7 +182,7 @@ function subscribeQuery(): void { const attributes: SpanAttributes = { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, 'knex.version': data.moduleVersion, [DB_SYSTEM_NAME]: dbSystem, [ATTR_DB_SQL_TABLE]: table, diff --git a/packages/server-utils/src/integrations/koa/index.ts b/packages/server-utils/src/integrations/koa/index.ts index 3ee982041147..e3f1c10690c0 100644 --- a/packages/server-utils/src/integrations/koa/index.ts +++ b/packages/server-utils/src/integrations/koa/index.ts @@ -10,10 +10,9 @@ import { getIsolationScope, hasSpanStreamingEnabled, ROUTER_SPAN_NAME_FALLBACK, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan, } from '@sentry/core'; -import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_TYPE, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { MIDDLEWARE, ROUTER } from '@sentry/conventions/op'; import { DEBUG_BUILD } from '../../debug-build'; import { CHANNELS } from '../../orchestrion/channels'; @@ -213,7 +212,7 @@ function patchLayer( attributes: { ...metadata.attributes, [SENTRY_OP]: layerType === LAYER_TYPE.MIDDLEWARE ? MIDDLEWARE : ROUTER, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }, }, () => { diff --git a/packages/server-utils/src/integrations/mistral.ts b/packages/server-utils/src/integrations/mistral.ts index 117f0f965c0b..f70d026cf358 100644 --- a/packages/server-utils/src/integrations/mistral.ts +++ b/packages/server-utils/src/integrations/mistral.ts @@ -1,11 +1,7 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; -import { - _INTERNAL_shouldSkipAiProviderWrapping, - defineIntegration, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startInactiveSpan, -} from '@sentry/core'; +import { _INTERNAL_shouldSkipAiProviderWrapping, defineIntegration, startInactiveSpan } from '@sentry/core'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; import { addRequestAttributes, extractRequestAttributes } from '../ai/mistral'; import { MISTRAL_INTEGRATION_NAME, MISTRAL_ORIGIN } from '../ai/mistral/constants'; @@ -86,7 +82,7 @@ function createGenAiSpan( const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractRequestAttributes(args, operation, recordInputs, streaming); - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = MISTRAL_ORIGIN; + attributes[SENTRY_ORIGIN] = MISTRAL_ORIGIN; const span = startInactiveSpan({ name: getSpanName(operation, attributes), diff --git a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts index 455b86653ac5..2f0a86721314 100644 --- a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts +++ b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts @@ -8,16 +8,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { - getClient, - hasSpanStreamingEnabled, - isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startInactiveSpan, -} from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, isObjectLike, startInactiveSpan } from '@sentry/core'; // `db.connection_string` is not part of `@sentry/conventions`, so it stays inlined to match // what `@opentelemetry/instrumentation-mongodb` emitted. @@ -127,7 +122,7 @@ export function getSpanAttributes( origin: string, ): SpanAttributes { const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [SENTRY_ORIGIN]: origin, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MONGODB, [DB_NAMESPACE]: dbName, [DB_COLLECTION_NAME]: dbCollection, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts index 7e4230b84fbe..9739ba070096 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-dc-subscriber.ts @@ -9,16 +9,10 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { - debug, - getClient, - hasSpanStreamingEnabled, - isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - startInactiveSpan, -} from '@sentry/core'; +import { debug, getClient, hasSpanStreamingEnabled, isObjectLike, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { bindTracingChannelToSpan } from '../../tracing-channel'; @@ -138,7 +132,7 @@ function setupChannel(tracingChannel: MongooseTracingChannelFactory, channelName return startInactiveSpan({ name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, [DB_OPERATION_NAME]: data.operation, diff --git a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts index b279343435f8..d22e7c8a1063 100644 --- a/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts +++ b/packages/server-utils/src/integrations/mongoose/mongoose-legacy-span.ts @@ -8,10 +8,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, startInactiveSpan } from '@sentry/core'; const DB_SYSTEM_NAME_VALUE_MONGODB = 'mongodb'; @@ -59,7 +60,7 @@ export function startMongooseLegacySpan({ [SERVER_PORT]: collection?.conn?.port, [DB_OPERATION_NAME]: operation, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: origin, + [SENTRY_ORIGIN]: origin, }; const client = getClient(); diff --git a/packages/server-utils/src/integrations/mysql.ts b/packages/server-utils/src/integrations/mysql.ts index ccbbad5a1565..e4049b65d30a 100644 --- a/packages/server-utils/src/integrations/mysql.ts +++ b/packages/server-utils/src/integrations/mysql.ts @@ -9,6 +9,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn, Scope } from '@sentry/core'; @@ -19,7 +20,6 @@ import { getClient, getCurrentScope, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; @@ -102,7 +102,7 @@ function instrumentMysql(): void { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.mysql', + [SENTRY_ORIGIN]: 'auto.db.mysql', [ATTR_DB_CONNECTION_STRING]: getJDBCString(host, portIsNumber ? portNumber : undefined, database), ...(database ? { [DB_NAMESPACE]: database } : {}), ...(user ? { [DB_USER]: user } : {}), diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index 4d01b681f1ce..b8f6501a26ba 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -5,7 +5,6 @@ import { getClient, hasSpanStreamingEnabled, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, waitForTracingChannelBinding, } from '@sentry/core'; @@ -26,6 +25,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; @@ -96,7 +96,7 @@ function subscribeQueryChannel(channelName: ChannelName): void { name, attributes: { [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, [DB_QUERY_TEXT]: queryText || undefined, diff --git a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts index 71264c73565f..6ba7d1699d01 100644 --- a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts @@ -8,9 +8,10 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; -import { getClient, hasSpanStreamingEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { getClient, hasSpanStreamingEnabled, startInactiveSpan } from '@sentry/core'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import { sanitizeSqlQueryWithSummary } from '../../utils/sql'; @@ -109,7 +110,7 @@ function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelN return startInactiveSpan({ name, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, [DB_QUERY_TEXT]: queryText, @@ -132,7 +133,7 @@ function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channe return startInactiveSpan({ name: spanName, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, [DB_NAMESPACE]: data.database || undefined, diff --git a/packages/server-utils/src/integrations/openai-compatible.ts b/packages/server-utils/src/integrations/openai-compatible.ts index be98c3de7a9e..2c23c711b60f 100644 --- a/packages/server-utils/src/integrations/openai-compatible.ts +++ b/packages/server-utils/src/integrations/openai-compatible.ts @@ -4,10 +4,9 @@ import { _INTERNAL_shouldSkipAiProviderWrapping, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; -import { GEN_AI_PROVIDER_NAME } from '@sentry/conventions/attributes'; +import { GEN_AI_PROVIDER_NAME, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; import { wrapApiPromiseResponse } from '../ai/core/apiPromise'; import { addRequestAttributes, extractRequestAttributes } from '../ai/openai'; @@ -117,7 +116,7 @@ function createGenAiSpan( // `extractRequestAttributes` defaults the provider to openai; override it for the concrete provider. const attributes = extractRequestAttributes(args, operation, recordInputs); attributes[GEN_AI_PROVIDER_NAME] = provider.providerName; - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = provider.origin; + attributes[SENTRY_ORIGIN] = provider.origin; const model = (params?.model as string) || 'unknown'; const client = getClient(); diff --git a/packages/server-utils/src/integrations/openai.ts b/packages/server-utils/src/integrations/openai.ts index ada6e60411a2..a5375794033e 100644 --- a/packages/server-utils/src/integrations/openai.ts +++ b/packages/server-utils/src/integrations/openai.ts @@ -1,3 +1,4 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn, Span, SpanAttributeValue } from '@sentry/core'; import { @@ -5,7 +6,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils'; @@ -93,7 +93,7 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti const { recordInputs } = resolveAIRecordingOptions(options); const attributes = extractRequestAttributes(args, operation, recordInputs); - attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN; + attributes[SENTRY_ORIGIN] = ORIGIN; const model = (params?.model as string) || 'unknown'; const client = getClient(); diff --git a/packages/server-utils/src/integrations/postgres-js.ts b/packages/server-utils/src/integrations/postgres-js.ts index afb8d5ecc099..5d3b865cd20d 100644 --- a/packages/server-utils/src/integrations/postgres-js.ts +++ b/packages/server-utils/src/integrations/postgres-js.ts @@ -7,6 +7,7 @@ import { ERROR_TYPE, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn, Span } from '@sentry/core'; @@ -15,7 +16,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; @@ -299,7 +299,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { attributes: { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_POSTGRES, [DB_QUERY_TEXT]: sanitizedSqlQuery, [DB_QUERY_SUMMARY]: querySummary, diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 1e7f0fdeb018..5d93ac17769f 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -9,6 +9,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import type { IntegrationFn, Scope, SpanAttributes } from '@sentry/core'; @@ -19,7 +20,6 @@ import { getClient, getCurrentScope, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; import { sanitizeSqlQueryWithSummary } from '../utils/sql'; @@ -191,7 +191,7 @@ function querySpanOptions(ctx: PgChannelContext): { name: string; attributes: Sp attributes: { [SENTRY_OP]: DB, ...getConnectionAttributes(params), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_QUERY_TEXT]: queryText || undefined, [DB_QUERY_SUMMARY]: querySummary, [ATTR_PG_PLAN]: typeof queryConfig?.name === 'string' ? queryConfig.name : undefined, diff --git a/packages/server-utils/src/integrations/prisma/orchestrion.ts b/packages/server-utils/src/integrations/prisma/orchestrion.ts index b85f7c7d8d45..656b7cf8fc75 100644 --- a/packages/server-utils/src/integrations/prisma/orchestrion.ts +++ b/packages/server-utils/src/integrations/prisma/orchestrion.ts @@ -1,14 +1,7 @@ import * as diagnosticsChannel from '../../utils/diagnosticsChannel'; import type { Span } from '@sentry/core'; -import { - getActiveSpan, - getRootSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - spanIsIgnored, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; -import { DB_COLLECTION_NAME, DB_OPERATION_NAME, SENTRY_OP } from '@sentry/conventions/attributes'; +import { getActiveSpan, getRootSpan, spanIsIgnored, startInactiveSpan, withActiveSpan } from '@sentry/core'; +import { DB_COLLECTION_NAME, DB_OPERATION_NAME, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { prismaChannels, PRISMA_LAZY_TERMINALS } from '../../orchestrion/config/prisma'; import type { TracingChannelLifeCycleOptions } from '../../tracing-channel'; @@ -84,7 +77,7 @@ function startOperationSpan(method: string, data: PrismaTerminalChannelContext): name: OPERATION_SPAN_NAME, parentSpan, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, + [SENTRY_ORIGIN]: PRISMA_ORIGIN, [SENTRY_OP]: DB, [DB_OPERATION_NAME]: method, [DB_COLLECTION_NAME]: table ?? model, diff --git a/packages/server-utils/src/integrations/prisma/tracing-helper.ts b/packages/server-utils/src/integrations/prisma/tracing-helper.ts index 0cfe6cf471d0..218e7c788134 100644 --- a/packages/server-utils/src/integrations/prisma/tracing-helper.ts +++ b/packages/server-utils/src/integrations/prisma/tracing-helper.ts @@ -20,7 +20,6 @@ import { getClient, hasSpanStreamingEnabled, LRUMap, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, startSpanManual, } from '@sentry/core'; @@ -34,6 +33,7 @@ import { DB_SYSTEM_NAME, SENTRY_KIND, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getSqlQuerySummary, sanitizeSqlQuery, type SqlDialect, toSqlDialect } from '../../utils/sql'; @@ -98,7 +98,7 @@ function registerPrismaSpan(id: string, span: Span): void { function buildSpanAttributes(name: string, attributes: Record | undefined): SpanAttributes { const merged: SpanAttributes = { ...(attributes as SpanAttributes | undefined), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: PRISMA_ORIGIN, + [SENTRY_ORIGIN]: PRISMA_ORIGIN, }; // Prisma itself emits the deprecated `db.system` on older versions, so both spellings are checked diff --git a/packages/server-utils/src/integrations/redis/index.ts b/packages/server-utils/src/integrations/redis/index.ts index fb19018aa598..4140156cee57 100644 --- a/packages/server-utils/src/integrations/redis/index.ts +++ b/packages/server-utils/src/integrations/redis/index.ts @@ -8,6 +8,7 @@ import { SERVER_ADDRESS, SERVER_PORT, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core'; @@ -15,7 +16,6 @@ import { isObjectLike, defineIntegration, getActiveSpan, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, withActiveSpan, @@ -104,7 +104,7 @@ function nodeRedisAttributes(options: NodeRedisClientOptions | undefined): SpanA [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_REDIS, [SERVER_ADDRESS]: host, ...(port != null ? { [SERVER_PORT]: port } : {}), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }; } @@ -160,7 +160,7 @@ function subscribeLegacyRedisCommand(cacheOptions: RedisCacheOptions): void { const client = data.self as LegacyRedisClient | undefined; const attributes: SpanAttributes = { [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_REDIS, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }; if (client?.connection_options?.host != null) { diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index f50060a29a8e..0e91d59c03bb 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -7,10 +7,11 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import { CHANNELS } from '../../orchestrion/channels'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import type { RedisCacheOptions } from './redis-cache'; @@ -50,7 +51,7 @@ function connectionAttributes(host: string | undefined, port: number | undefined [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_REDIS, ...(host != null ? { [SERVER_ADDRESS]: host } : {}), ...(port != null ? { [SERVER_PORT]: port } : {}), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, }; } diff --git a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts index 090b02d7272e..de092c109bd7 100644 --- a/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/redis-dc-subscriber.ts @@ -7,10 +7,11 @@ import { SERVER_ADDRESS, SERVER_PORT, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB_QUERY, DB } from '@sentry/conventions/op'; import type { SpanAttributes } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import { bindTracingChannelToSpan } from '../../tracing-channel'; import type { RedisCacheOptions } from './redis-cache'; import { applyCacheResponseAttributes, getRedisCacheAttributes } from './redis-cache'; @@ -148,7 +149,7 @@ function setupCommandChannel( port: data.serverPort, }); const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB_QUERY, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_REDIS, [DB_OPERATION_NAME]: data.command, @@ -182,7 +183,7 @@ function setupBatchChannel( return startInactiveSpan({ name: operation, attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB_QUERY, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_REDIS, [DB_OPERATION_NAME]: operation, @@ -201,7 +202,7 @@ function setupConnectChannel(tracingChannel: RedisTracingChannelFactory, channel return startInactiveSpan({ name: 'redis-connect', attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: DB, [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_REDIS, ...(data.serverAddress != null ? { [SERVER_ADDRESS]: data.serverAddress } : {}), diff --git a/packages/server-utils/src/integrations/tedious.ts b/packages/server-utils/src/integrations/tedious.ts index a2b3ea352791..21ff279dd13f 100644 --- a/packages/server-utils/src/integrations/tedious.ts +++ b/packages/server-utils/src/integrations/tedious.ts @@ -9,7 +9,6 @@ import { defineIntegration, getClient, hasSpanStreamingEnabled, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, startInactiveSpan, } from '@sentry/core'; @@ -23,6 +22,7 @@ import { SENTRY_OP, SERVER_ADDRESS, SERVER_PORT, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { DB } from '@sentry/conventions/op'; import { CHANNELS } from '../orchestrion/channels'; @@ -138,7 +138,7 @@ function subscribeQuery(channelName: string, operation: string): void { const attributes: SpanAttributes = { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MSSQL, [DB_NAMESPACE]: databaseName, // `>=4` uses the `authentication` object; older versions expose `userName` directly. diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 78b4763f0240..d1b420635876 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -25,6 +25,7 @@ import { GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS, @@ -39,7 +40,6 @@ import { captureException, getClient, isObjectLike, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, spanToJSON, spanToTraceContext, @@ -418,7 +418,7 @@ export function createSpanFromMessage( } const baseAttributes: Record = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, ...(provider ? { [GEN_AI_PROVIDER_NAME]: provider, [VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE]: provider } : {}), ...(modelId ? { [GEN_AI_REQUEST_MODEL]: modelId } : {}), ...(maxRetries !== undefined ? { [VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE]: maxRetries } : {}), @@ -533,7 +533,7 @@ function buildToolSpan(event: Record, recordInputs: boolean): S const description = recordInputs && toolName ? resolveToolDescription(asString(event.callId), toolName, event.tools) : undefined; return startGenAiSpan('execute_tool', toolName, { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN, + [SENTRY_ORIGIN]: ORIGIN, ...(toolName ? { [GEN_AI_TOOL_NAME]: toolName } : {}), ...(toolCallId ? { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: toolCallId } : {}), ...(description ? { [GEN_AI_TOOL_DESCRIPTION]: description } : {}), diff --git a/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts b/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts index 8e7c4d840d11..f2784635c5f2 100644 --- a/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/workers-ai.test.ts @@ -12,11 +12,11 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getMainCarrier, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, spanToStaticSpanJSON, @@ -69,7 +69,7 @@ describe('instrumentWorkersAiClient', () => { * are on here because the `run` call is the root span in these tests, with no active parent. */ const ALWAYS_RECORDED = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', + [SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', [SENTRY_OP]: 'gen_ai.chat', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', @@ -187,7 +187,7 @@ describe('instrumentWorkersAiClient', () => { const span = spanToStaticSpanJSON(endedSpans[0]!); expect(span.description).toBe('evaluate typesafe/jev'); expect(span.data).toEqual({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', + [SENTRY_ORIGIN]: 'auto.ai.cloudflare.workers_ai', [SENTRY_OP]: 'gen_ai.evaluate', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SENTRY_SEGMENT_NAME_SOURCE]: 'custom', diff --git a/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts b/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts index 57a43b0a80ff..9976ff52706a 100644 --- a/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts +++ b/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { Span } from '@sentry/core'; import { GEN_AI_PROVIDER_NAME, @@ -21,6 +20,7 @@ import { GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../../../../src/ai/core/gen-ai-attributes'; import { WORKERS_AI_ORIGIN, WORKERS_AI_PROVIDER_NAME } from '../../../../src/ai/workers-ai/constants'; @@ -89,7 +89,7 @@ describe('workers-ai utils', () => { expect(extractRequestAttributes(MODEL, { prompt: 'Hello' }, 'chat')).toEqual({ [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, + [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, [GEN_AI_REQUEST_MODEL]: MODEL, }); }); @@ -113,7 +113,7 @@ describe('workers-ai utils', () => { ).toEqual({ [GEN_AI_PROVIDER_NAME]: WORKERS_AI_PROVIDER_NAME, [GEN_AI_OPERATION_NAME]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, + [SENTRY_ORIGIN]: WORKERS_AI_ORIGIN, [GEN_AI_REQUEST_MODEL]: MODEL, [GEN_AI_REQUEST_TEMPERATURE]: 0.5, [GEN_AI_REQUEST_MAX_TOKENS]: 100, From 9a47ffe06b2bcfaaab2c530eb7ea0bb951f2d879 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 5 Oct 2026 13:19:06 +0200 Subject: [PATCH 16/73] fix(feedback): Correct overlapping screenshot annotations and drag dimming (#25040) Fix screenshot annotations starting at the wrong position when a drag begins inside an existing box, and prevent the background from getting darker while dragging. Mouse-down now uses image-relative coordinates consistently, and highlight shadows no longer leak into the next canvas repaint. reported here: https://sentry.slack.com/archives/CDXAKMGTU/p1790972462069039 Before/after from a local reproduction with generated sample content. Both captures show the same drag with the mouse held down; red markers indicate its start and cursor positions. | Before | After | | --- | --- | | ![Before](https://github.com/user-attachments/assets/6db1d723-f00b-49ba-bb4b-f82719cc2d69) | ![After](https://github.com/user-attachments/assets/dabac060-1b21-45e8-83c4-5429fa41dbd6) | --------- Co-authored-by: OpenAI Codex --- .../suites/feedback/screenshotEditor/init.js | 7 ++ .../suites/feedback/screenshotEditor/test.ts | 81 +++++++++++++++++++ .../components/ScreenshotEditor.tsx | 6 +- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js create mode 100644 dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts diff --git a/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js new file mode 100644 index 000000000000..94453fb8e47a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/init.js @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/browser'; +import { feedbackIntegration } from '@sentry/browser'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [feedbackIntegration({ colorScheme: 'light' })], +}); diff --git a/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts new file mode 100644 index 000000000000..e0d177283073 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/feedback/screenshotEditor/test.ts @@ -0,0 +1,81 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipFeedbackTest } from '../../../utils/helpers'; + +async function drawBox(page: Page, from: { x: number; y: number }, to: { x: number; y: number }): Promise { + await page.mouse.move(from.x, from.y); + await page.mouse.down(); + await page.mouse.move(to.x, to.y, { steps: 3 }); + await page.mouse.up(); +} + +sentryTest.beforeEach(async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipFeedbackTest()); + sentryTest.skip(browserName !== 'chromium', 'The canvas screen-capture fixture requires Chromium.'); + + await page.addInitScript(() => { + // The fixture runs over HTTP, but screenshot capture is stubbed below. + Object.defineProperty(window, 'isSecureContext', { value: true }); + Object.defineProperty(navigator, 'mediaDevices', { + value: { + getDisplayMedia: async () => { + const canvas = document.createElement('canvas'); + canvas.width = 1600; + canvas.height = 1000; + const ctx = canvas.getContext('2d')!; + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + return canvas.captureStream(); + }, + }, + configurable: true, + }); + }); + + await page.goto(await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true })); + await page.getByRole('button', { name: 'Report a Bug', exact: true }).click(); + await page.getByRole('button', { name: 'Add a screenshot', exact: true }).click(); + await expect(page.locator('#foreground')).toBeVisible(); + await expect.poll(() => page.locator('#background').evaluate((canvas: HTMLCanvasElement) => canvas.width)).toBe(1600); +}); + +sentryTest('starts a highlight at the cursor inside an existing box', async ({ page }) => { + const canvas = page.locator('#foreground'); + const bounds = (await canvas.boundingBox())!; + const point = (x: number, y: number) => ({ x: bounds.x + x, y: bounds.y + y }); + await drawBox(page, point(80, 60), point(280, 240)); + await expect(page.locator('.editor__rect')).toHaveCount(1); + const start = point(160, 140); + const end = point(240, 200); + + await drawBox(page, start, end); + + await expect(page.locator('.editor__rect')).toHaveCount(2); + const box = (await page.locator('.editor__rect').nth(1).boundingBox())!; + expect(box.x).toBeCloseTo(start.x, 0); + expect(box.y).toBeCloseTo(start.y, 0); + expect(box.width).toBeCloseTo(end.x - start.x, 0); + expect(box.height).toBeCloseTo(end.y - start.y, 0); +}); + +sentryTest('keeps background dimming unchanged while dragging another box', async ({ page }) => { + const canvas = page.locator('#foreground'); + const bounds = (await canvas.boundingBox())!; + const backgroundPixel = () => + canvas.evaluate((element: HTMLCanvasElement) => + Array.from(element.getContext('2d')!.getImageData(1400, 800, 1, 1).data), + ); + await drawBox(page, { x: bounds.x + 80, y: bounds.y + 60 }, { x: bounds.x + 240, y: bounds.y + 180 }); + await expect(page.locator('.editor__rect')).toHaveCount(1); + await expect.poll(backgroundPixel).toEqual([0, 0, 0, 64]); + + await page.mouse.move(bounds.x + 280, bounds.y + 60); + await page.mouse.down(); + await page.mouse.move(bounds.x + 360, bounds.y + 180, { steps: 3 }); + + expect(await backgroundPixel()).toEqual([0, 0, 0, 64]); + await page.mouse.up(); + await expect(page.locator('.editor__rect')).toHaveCount(2); + await expect.poll(backgroundPixel).toEqual([0, 0, 0, 64]); +}); diff --git a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx index 4c4ec2510b87..414250dbf4a0 100644 --- a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx +++ b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx @@ -50,6 +50,7 @@ interface DrawCommand { function drawRect(command: DrawCommand, ctx: CanvasRenderingContext2D, color: string): void { switch (command.type) { case 'highlight': { + ctx.save(); // creates a shadow around ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; ctx.shadowBlur = 50; @@ -60,6 +61,7 @@ function drawRect(command: DrawCommand, ctx: CanvasRenderingContext2D, color: st // cut out the inside of the rectangle ctx.clearRect(command.x, command.y, command.w, command.h); + ctx.restore(); break; } @@ -229,8 +231,8 @@ export function ScreenshotEditorFactory({ const boundingRect = mouseRef.current.getBoundingClientRect(); const startingPoint: DrawCommand = { type: action, - x: e.offsetX / scaleFactor, - y: e.offsetY / scaleFactor, + x: (e.clientX - boundingRect.x) / scaleFactor, + y: (e.clientY - boundingRect.y) / scaleFactor, w: 0, h: 0, }; From 79d84ff345b5fe5ed329a218094107ed5be7618a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 13:21:22 +0200 Subject: [PATCH 17/73] ref(core): Deprecate SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN (#25039) Marks `SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN` as deprecated in favor of `SENTRY_ORIGIN` from `@sentry/conventions/attributes`. This PR finalizes the replacement of all SDK constants that are available in the conventions package. Fixes #24961 / [JS-3868](https://linear.app/getsentry/issue/JS-3868). Co-authored-by: GPT-6 --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/cloudflare/src/index.ts | 1 + packages/core/src/semanticAttributes.ts | 1 + packages/deno/src/index.ts | 1 + packages/elysia/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/cloudflare/index.ts | 1 + packages/remix/src/server/index.ts | 1 + packages/solidstart/src/server/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/sveltekit/src/worker/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 16 files changed, 16 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index fcdbc89ee7f6..ed0b1b540bb4 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -122,6 +122,7 @@ export { SDK_VERSION, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 77b40c7da02d..6f17bcc02c0c 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -97,6 +97,7 @@ export { parameterize, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index cf066ee57cd2..48b09d3c1acc 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -80,6 +80,7 @@ export { export { // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, } from '@sentry/core'; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index e02142b940d6..e96e052656cb 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -118,6 +118,7 @@ export { parameterize, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 0b583ec88d9b..1d3aef44a03c 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -88,6 +88,7 @@ export { consoleIntegration, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToStaticSpanJSON, diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 5f3ed230f85e..c9e64cc6e781 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -22,6 +22,7 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_OP = 'sentry.op'; /** * Use this attribute to represent the origin of a span. + * @deprecated Use `SENTRY_ORIGIN` from `@sentry/conventions/attributes` instead. */ export const SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN = 'sentry.origin'; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index cccdbf5f479c..090eca082909 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -83,6 +83,7 @@ export { zodErrorsIntegration, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, startSession, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index e5ddb8ee3726..17baec308ba1 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -97,6 +97,7 @@ export { parameterize, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 8404d252e4b3..eb4fa0495540 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -98,6 +98,7 @@ export { parameterize, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 4ca920d43784..1a1105557d9f 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -115,6 +115,7 @@ export { setConversationId, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setCurrentClient, diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 36629df62a35..e012093aaf76 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -108,6 +108,7 @@ export { zodErrorsIntegration, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToStaticSpanJSON, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 00bc981926d9..d35dceb18969 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -86,6 +86,7 @@ export { SDK_VERSION, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index d8a151159551..8c8576aeb7ae 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -90,6 +90,7 @@ export { SDK_VERSION, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index a0883d508e5b..cb0730b4c513 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -89,6 +89,7 @@ export { SDK_VERSION, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 1730aac31865..c7b6c5b11d54 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -55,6 +55,7 @@ export { SDK_VERSION, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, setContext, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 6d0b6e6a0962..abe436f67705 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -85,6 +85,7 @@ export { consoleIntegration, // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_OP, + // oxlint-disable-next-line typescript/no-deprecated SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, spanToStaticSpanJSON, From 3a7ad4b9a089055d08b28218e296b467be4fbeee Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:33:07 +0200 Subject: [PATCH 18/73] feat(nextjs): Add `code.file.path` to `use cache` fill spans (#24988) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `code.file.path` to `cache.put` spans, so `use cache` spans show which function they belong to. The cache key contains the function id. Next.js' server-reference manifest maps it to the source file. Keys come in two forms: - JSON: `["buildId","c0a941ad…",[args]]` (start with `[`) - multipart, when args don't serialize to JSON (`params`, `children`): `1:069:["buildId","c0a941ad…",[…]]1:1c:{"id":"123"}` (length-prefixed fields) Logged real-world data to use this in the unit tests (from 16.3 and canary version). Linear: https://linear.app/getsentry/issue/JSSDK-31/add-cache-source-file-to-trace-back-where-the-cache-was-created --- .../nextjs-16-cacheComponents/package.json | 2 +- .../tests/cacheOriginLinks-nesting.spec.ts | 2 + .../tests/useCacheSpans.spec.ts | 15 +- .../package.json | 2 +- .../tests/cacheOriginLinks-nesting.spec.ts | 10 + .../tests/useCacheSpans.spec.ts | 34 +++ .../src/server/useCacheInstrumentation.ts | 16 +- .../nextjs/src/server/useCacheSourceFile.ts | 134 ++++++++++ .../server/useCacheInstrumentation.test.ts | 58 +++++ .../test/server/useCacheSourceFile.test.ts | 236 ++++++++++++++++++ 10 files changed, 503 insertions(+), 6 deletions(-) create mode 100644 packages/nextjs/src/server/useCacheSourceFile.ts create mode 100644 packages/nextjs/test/server/useCacheSourceFile.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json index 899d3d4e6c87..309cb063a0a5 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/package.json @@ -20,7 +20,7 @@ "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack", "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack", "test:assert": "pnpm test:prod && pnpm test:dev", - "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" + "test:assert-webpack": "TEST_BUNDLER=webpack pnpm test:prod && pnpm test:dev-webpack" }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts index 9c6245a06f5c..ad7b3670c42a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -35,6 +35,8 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn const putSpans = (missTx.spans ?? []).filter(span => span.op === 'cache.put'); expect(new Set(putSpans.map(span => span.description)).size).toBe(1); + expect(putSpans[0]!.data?.['code.file.path']).toBe('app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx'); + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true); expect(hitGetSpan).toBeDefined(); expect(hitGetSpan?.description).toBe(putSpans[0]!.description); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts index dadb754e0a9b..9b93cef32218 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts @@ -1,6 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +// Webpack lists route handler `use cache` functions in the server-reference manifest, Turbopack +// (Next.js 16.3) does not. `TEST_BUNDLER` marks the webpack variant's production run (set in +// `test:assert-webpack`). +const isWebpackBuild = process.env.TEST_BUNDLER === 'webpack' || process.env.TEST_ENV === 'development-webpack'; +const expectedRouteHandlerFilePath = isWebpackBuild ? 'app/api/use-cache/route.ts' : undefined; + test('Should create cache spans around `use cache` functions', async ({ request }) => { // A fresh id makes the first request a guaranteed cache miss (the id is part of the cache key) // even when the test is retried against the same server. @@ -57,6 +63,8 @@ test('Should create cache spans around `use cache` functions', async ({ request }), }); + expect(putSpan!.data?.['code.file.path']).toBe(expectedRouteHandlerFilePath); + const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get'); expect(hitGetSpan).toBeDefined(); expect(hitGetSpan).toMatchObject({ @@ -97,7 +105,12 @@ test('Should create cache spans for `use cache` inside a rendered page', async ( await request.get(`/use-cache-page?id=${id}`); const hitTx = await hitTxPromise; - expect(missTx.spans?.some(span => span.op === 'cache.put')).toBe(true); + // The source file on a fill span marks which cached function produced the entry. + const missPutSpans = missTx.spans?.filter(span => span.op === 'cache.put') ?? []; + expect(missPutSpans.length).toBeGreaterThan(0); + for (const putSpan of missPutSpans) { + expect(putSpan.data?.['code.file.path']).toBe('app/use-cache-page/page.tsx'); + } // A render can read more than one cache entry, so look at every hit instead of the first `cache.get`. const hitGetSpans = hitTx.spans?.filter(span => span.op === 'cache.get' && span.data?.['cache.hit'] === true) ?? []; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json index 08d3348c689f..d6b1d3873afd 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json @@ -20,7 +20,7 @@ "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack", "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack", "test:assert": "pnpm test:prod && pnpm test:dev", - "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack" + "test:assert-webpack": "TEST_BUNDLER=webpack pnpm test:prod && pnpm test:dev-webpack" }, "dependencies": { "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts index abd984c429dc..828e867ae3d7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-nesting.spec.ts @@ -36,6 +36,8 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn const putSpan = findCacheSpan(missSpans, 'cache.put'); expect(putSpan).toBeDefined(); + expect(putSpan!.attributes['code.file.path']?.value).toBe('app/(cached-nesting)/cached-mid-layout/[id]/layout.tsx'); + const hitGetSpan = findCacheSpan(hitSpans, 'cache.get', true); expect(hitGetSpan).toBeDefined(); expect(hitGetSpan!.attributes['cache.key']).toEqual(putSpan!.attributes['cache.key']); @@ -114,6 +116,14 @@ test('links two cached levels to different origin traces after the layout expire const fillPutSpans = fillSpans.filter(span => getSpanOp(span) === 'cache.put'); expect(new Set(fillPutSpans.map(span => JSON.stringify(span.attributes['cache.key']?.value))).size).toBe(2); + // One fill per file: the layout (multipart key) and the cached component (JSON key). + expect(new Set(fillPutSpans.map(span => span.attributes['code.file.path']?.value))).toEqual( + new Set([ + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + 'app/(cached-nesting)/mixed-lifetimes/[id]/page.tsx', + ]), + ); + // Sleep past the layout's `expire` (2s); the component entry stays valid for hours. await new Promise(resolve => setTimeout(resolve, 3_000)); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts index 2704de1a11f6..c4f7a5c5b8ca 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts @@ -2,6 +2,12 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; import { findCacheSpan } from './cacheOriginLinks-utils'; +// Webpack lists route handler `use cache` functions in the server-reference manifest, Turbopack +// (Next.js 16.3) does not. `TEST_BUNDLER` marks the webpack variant's production run (set in +// `test:assert-webpack`). +const isWebpackBuild = process.env.TEST_BUNDLER === 'webpack' || process.env.TEST_ENV === 'development-webpack'; +const expectedRouteHandlerFilePath = isWebpackBuild ? 'app/api/use-cache/route.ts' : undefined; + test('uses low-cardinality names for `use cache` spans', async ({ request }) => { // A fresh id makes the request a guaranteed cache miss (the id is part of the cache key), so the // trace contains both a `cache.get` and a `cache.put` span. @@ -30,4 +36,32 @@ test('uses low-cardinality names for `use cache` spans', async ({ request }) => expect(putSpan).toBeDefined(); expect(putSpan!.name).toBe('cache.put'); expect(putSpan!.attributes['cache.key']?.value).toEqual(cacheKeyDigest); + + expect(putSpan!.attributes['code.file.path']?.value).toBe(expectedRouteHandlerFilePath); +}); + +test('sets the source file of the cached component on `cache.put` spans', async ({ request }) => { + const id = crypto.randomUUID(); + + const spansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => { + return ( + spansOfTrace.some(span => span.name === 'GET /cached-sibling-components' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'cache.put').length >= 2 + ); + }); + + await request.get(`/cached-sibling-components?id=${id}`); + const spans = await spansPromise; + + // Both sibling entries come from the same page file. + const putSpans = spans.filter(span => getSpanOp(span) === 'cache.put'); + expect(putSpans.length).toBeGreaterThanOrEqual(2); + for (const putSpan of putSpans) { + expect(putSpan.attributes['code.file.path']?.value).toBe('app/cached-sibling-components/page.tsx'); + } + + // The source file marks the producer of an entry. Reads do not carry it. + const getSpan = findCacheSpan(spans, 'cache.get'); + expect(getSpan).toBeDefined(); + expect(getSpan!.attributes['code.file.path']).toBeUndefined(); }); diff --git a/packages/nextjs/src/server/useCacheInstrumentation.ts b/packages/nextjs/src/server/useCacheInstrumentation.ts index ac0a32373790..e51e5e148e05 100644 --- a/packages/nextjs/src/server/useCacheInstrumentation.ts +++ b/packages/nextjs/src/server/useCacheInstrumentation.ts @@ -6,6 +6,7 @@ import { CACHE_OPERATION, CACHE_TAGS, CACHE_TTL, + CODE_FILE_PATH, SENTRY_LINK_TYPE, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; @@ -26,6 +27,7 @@ import { timestampInSeconds, } from '@sentry/core'; import { DEBUG_BUILD } from '../common/debug-build'; +import { getCacheFunctionSourceFile } from './useCacheSourceFile'; // Next.js shares its `use cache` handlers across bundles via `globalThis` // (`next/src/server/use-cache/handlers.ts`). This module can load once per bundle, so all @@ -102,7 +104,12 @@ function shouldRecordCacheSpan(): boolean { return !!activeSpan && spanIsSampled(activeSpan); } -function startCacheSpan(op: typeof CACHE_GET | typeof CACHE_PUT, digest: string, callback: (span: Span) => T): T { +function startCacheSpan( + op: typeof CACHE_GET | typeof CACHE_PUT, + digest: string, + extraAttributes: Record, + callback: (span: Span) => T, +): T { const client = getClient(); return startSpan( @@ -114,6 +121,7 @@ function startCacheSpan(op: typeof CACHE_GET | typeof CACHE_PUT, digest: stri [SENTRY_ORIGIN]: CACHE_SPAN_ORIGIN, [CACHE_KEY]: [digest], [CACHE_OPERATION]: CACHE_OPERATION_NAMES[op], + ...extraAttributes, }, }, callback, @@ -250,7 +258,7 @@ function instrumentHandler(handler: unknown): void { return originalGet.call(this, cacheKey, softTags); } const digest = keyDigest(cacheKey); - return startCacheSpan(CACHE_GET, digest, span => + return startCacheSpan(CACHE_GET, digest, {}, span => // `Promise.resolve` because custom handlers may return the entry synchronously. Promise.resolve(originalGet.call(this, cacheKey, softTags)).then(entry => { try { @@ -273,9 +281,11 @@ function instrumentHandler(handler: unknown): void { } const digest = keyDigest(cacheKey); + const sourceFile = getCacheFunctionSourceFile(cacheKey); + // The handler drains `pendingEntry` (the still-streaming entry) before storing, so this // span covers producing and storing the entry, not just the write. - return startCacheSpan(CACHE_PUT, digest, span => + return startCacheSpan(CACHE_PUT, digest, sourceFile ? { [CODE_FILE_PATH]: sourceFile } : {}, span => // Only a successful write becomes a fill origin: a failed write leaves no entry or the // previous one (whose origin still stands). A dropped span (`ignoreSpans`) never // reaches Sentry, so a link to it would be broken. diff --git a/packages/nextjs/src/server/useCacheSourceFile.ts b/packages/nextjs/src/server/useCacheSourceFile.ts new file mode 100644 index 000000000000..0530a0cd3495 --- /dev/null +++ b/packages/nextjs/src/server/useCacheSourceFile.ts @@ -0,0 +1,134 @@ +// Next.js' runtime manifest registry +// https://github.com/vercel/next.js/blob/8e0700c74474498a07b33f58da1c1316f740eb19/packages/next/src/server/app-render/manifests-singleton.ts#L56-L59 +const NEXT_MANIFESTS_SINGLETON = Symbol.for('next.server.manifests'); + +type ManifestEntries = Record; + +type GlobalWithManifests = typeof globalThis & { + [NEXT_MANIFESTS_SINGLETON]?: { + serverActionsManifest?: { + node?: ManifestEntries; + edge?: ManifestEntries; + }; + }; +}; + +interface MultipartField { + content: string; + /** Index of the first character after this field. */ + end: number; +} + +/** + * Reads one field of a multipart cache key, starting at `start`. + * A field is `:`, with the length in lowercase hex counting UTF-16 code units. + * https://github.com/vercel/next.js/blob/8e0700c74474498a07b33f58da1c1316f740eb19/packages/next/src/server/use-cache/use-cache-wrapper.ts#L1815-L1847 + * + * Returns `undefined` when the framing is broken. + */ +function readMultipartField(cacheKey: string, start: number): MultipartField | undefined { + // The characters before the next `:` must be a non-empty hex length. + const colon = cacheKey.indexOf(':', start); + if (colon <= start || !/^[0-9a-f]+$/.test(cacheKey.slice(start, colon))) { + return undefined; + } + + // A length that points past the end of the key means the key is truncated. + const contentStart = colon + 1; + const contentEnd = contentStart + parseInt(cacheKey.slice(start, colon), 16); + if (contentEnd > cacheKey.length) { + return undefined; + } + + return { content: cacheKey.slice(contentStart, contentEnd), end: contentEnd }; +} + +/** + * Cache keys with arguments that do not serialize to JSON (a page's `params` promise, a layout's + * `children`) are serialized FormData: pairs of length-prefixed fields, a field name followed by + * its content. React stores the key parts JSON in the field named `"0"`, which is not necessarily + * the first field. Returns that JSON text, or `undefined` for a malformed key. + */ +function readKeyPartsFromMultipartKey(cacheKey: string): string | undefined { + let position = 0; + while (position < cacheKey.length) { + // Each pair is the field name, then the field content. + const name = readMultipartField(cacheKey, position); + const content = name && readMultipartField(cacheKey, name.end); + if (name === undefined || content === undefined) { + return undefined; + } + + if (name.content === '0') { + return content.content; + } + position = content.end; + } + return undefined; +} + +/** + * Picks the function id out of the decoded key parts. Next.js 16.3 puts the id at index 1 + * (`[buildId, id, args]` in prod; dev appends a fourth part). 16.4 canary moves it to index 0 + * (`[id, args, …]`). Returns `undefined` for any other shape. + */ +function readFunctionIdFromKeyParts(keyParts: unknown): string | undefined { + if (!Array.isArray(keyParts)) { + return undefined; + } + // An args array at index 1 marks the canary shape, where the id sits at index 0. + const functionId = Array.isArray(keyParts[1]) ? keyParts[0] : keyParts[1]; + return typeof functionId === 'string' ? functionId : undefined; +} + +/** + * Manifest filenames start at the repo root, but only the path inside the project is useful. + * The repo-root-to-project part equals the tail of `process.cwd()`, because `next dev`, + * `next start`, and the standalone server all run in the project directory (the standalone + * `server.js` chdirs into its mirrored copy). Unknown layouts keep the full path. + */ +function toProjectRelativePath(filename: string): string { + // Split the cwd on both separators so Windows paths work. The manifest always uses `/`. + const cwdSegments = process.cwd().split(/[\\/]/).filter(Boolean); + const fileSegments = filename.split('/'); + + // Drop the longest filename prefix that matches the cwd tail. Longest first, so the whole + // repo prefix goes, not just a part of it. At least one segment always remains. + const maxOverlap = Math.min(fileSegments.length - 1, cwdSegments.length); + for (let overlap = maxOverlap; overlap > 0; overlap--) { + const cwdTail = cwdSegments.slice(-overlap); + if (cwdTail.every((segment, index) => segment === fileSegments[index])) { + return fileSegments.slice(overlap).join('/'); + } + } + return filename; +} + +/** + * Resolves the source file of the `use cache` function behind a cache key, through the server-reference manifest. + * With Turbopack on Next.js 16.3 the manifest only covers component-tree functions, so route handlers resolve + * to `undefined`. Webpack and 16.4 canary include route handlers. + * Any unexpected key or manifest shape returns `undefined`, never a wrong file. + */ +export function getCacheFunctionSourceFile(cacheKey: string): string | undefined { + try { + // The key is `encodeReply(keyParts)`: a plain JSON array when all function arguments + // serialize to JSON, the multipart form otherwise. + const keyPartsJson = cacheKey.startsWith('[') ? cacheKey : readKeyPartsFromMultipartKey(cacheKey); + if (!keyPartsJson) { + return undefined; + } + + const functionId = readFunctionIdFromKeyParts(JSON.parse(keyPartsJson)); + if (functionId === undefined) { + return undefined; + } + + // The manifest has one section per runtime. `NEXT_RUNTIME` is only `'edge'` on edge. + const manifest = (globalThis as GlobalWithManifests)[NEXT_MANIFESTS_SINGLETON]?.serverActionsManifest; + const filename = manifest?.[process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node']?.[functionId]?.filename; + return typeof filename === 'string' && filename ? toProjectRelativePath(filename) : undefined; + } catch { + return undefined; + } +} diff --git a/packages/nextjs/test/server/useCacheInstrumentation.test.ts b/packages/nextjs/test/server/useCacheInstrumentation.test.ts index 51037a67c005..587b57bf9820 100644 --- a/packages/nextjs/test/server/useCacheInstrumentation.test.ts +++ b/packages/nextjs/test/server/useCacheInstrumentation.test.ts @@ -33,6 +33,7 @@ import { _instrumentUseCacheHandlers } from '../../src/server/useCacheInstrument const NEXT_CACHE_HANDLERS_MAP = Symbol.for('@next/cache-handlers-map'); const NEXT_PRIVATE_CACHE_HANDLER = Symbol.for('@next/cache-handlers-private'); +const NEXT_MANIFESTS_SINGLETON = Symbol.for('next.server.manifests'); const SENTRY_CACHE_INSTRUMENTED = Symbol.for('sentry.nextjs.cacheHandlersInstrumented'); const SENTRY_WRAPPED_HANDLERS = Symbol.for('sentry.nextjs.wrappedCacheHandlers'); const SENTRY_CACHE_ORIGINS = Symbol.for('sentry.nextjs.cacheOrigins'); @@ -77,6 +78,7 @@ describe('instrumentUseCacheHandlers', () => { for (const symbol of [ NEXT_CACHE_HANDLERS_MAP, NEXT_PRIVATE_CACHE_HANDLER, + NEXT_MANIFESTS_SINGLETON, SENTRY_CACHE_INSTRUMENTED, SENTRY_WRAPPED_HANDLERS, SENTRY_CACHE_ORIGINS, @@ -484,6 +486,62 @@ describe('instrumentUseCacheHandlers', () => { }); }); + describe('source file on `cache.put`', () => { + const functionId = 'c05120808bb68f6400d039e720226869fb1f079019'; + const jsonCacheKey = JSON.stringify(['build-id', functionId, [['arg'], {}]]); + + function setManifest(filename: unknown): void { + setGlobal(NEXT_MANIFESTS_SINGLETON, { + serverActionsManifest: { node: { [functionId]: { filename } } }, + }); + } + + it('starts the `cache.put` span with `code.file.path` when the key parses and the manifest knows the function', async () => { + setManifest('app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx'); + const handler = installWithDefaultHandler(); + + await handler.set(jsonCacheKey, Promise.resolve({})); + + // In the start options (not set afterwards), so samplers and span processors see it. + expect(mocks.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + op: 'cache.put', + attributes: expect.objectContaining({ + 'code.file.path': 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + }), + }), + expect.any(Function), + ); + }); + + it('does not put `code.file.path` on `cache.get` spans', async () => { + setManifest('app/page.tsx'); + const handler = installWithDefaultHandler({ timestamp: nowMs() }); + + await handler.get(jsonCacheKey); + + expect(mocks.startSpan).not.toHaveBeenCalledWith( + expect.objectContaining({ attributes: expect.objectContaining({ 'code.file.path': expect.anything() }) }), + expect.any(Function), + ); + expect(mocks.setAttribute).not.toHaveBeenCalledWith('code.file.path', expect.anything()); + }); + + // Key parsing, manifest lookup, and path shortening are covered in `useCacheSourceFile.test.ts`. + it('still writes the entry and omits `code.file.path` when the key does not resolve', async () => { + setManifest('app/page.tsx'); + const handler = installWithDefaultHandler(); + + await expect(handler.set('multipart-encoded-key', Promise.resolve({}))).resolves.toBeUndefined(); + + expect(mocks.startSpan).not.toHaveBeenCalledWith( + expect.objectContaining({ attributes: expect.objectContaining({ 'code.file.path': expect.anything() }) }), + expect.any(Function), + ); + expect(mocks.setAttribute).not.toHaveBeenCalledWith('code.file.path', expect.anything()); + }); + }); + it('creates a `cache.put` span around handler writes', async () => { const handler = installWithDefaultHandler(); diff --git a/packages/nextjs/test/server/useCacheSourceFile.test.ts b/packages/nextjs/test/server/useCacheSourceFile.test.ts new file mode 100644 index 000000000000..16082bcf21b2 --- /dev/null +++ b/packages/nextjs/test/server/useCacheSourceFile.test.ts @@ -0,0 +1,236 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getCacheFunctionSourceFile } from '../../src/server/useCacheSourceFile'; + +const NEXT_MANIFESTS_SINGLETON = Symbol.for('next.server.manifests'); + +const FUNCTION_ID = 'c05120808bb68f6400d039e720226869fb1f079019'; +const LAYOUT_FILE = 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx'; + +const stableKey = JSON.stringify(['build-id', FUNCTION_ID, [['arg'], { locale: 'en' }]]); +const canaryKey = JSON.stringify([FUNCTION_ID, [['arg'], { locale: 'en' }], 'implementation-hash']); +const rootPayload = JSON.stringify([ + 'build-id', + FUNCTION_ID, + [[], { children: { then: '$undefined' }, params: '$@1' }], +]); + +function setManifest(filename: unknown, runtime: 'node' | 'edge' = 'node', functionId: string = FUNCTION_ID): void { + (globalThis as Record)[NEXT_MANIFESTS_SINGLETON] = { + serverActionsManifest: { [runtime]: { [functionId]: { filename } } }, + }; +} + +// Mirrors `encodeFormData` in Next.js' `use-cache-wrapper.ts`: each field is +// `:`, lengths counting UTF-16 code units. +function multipartField(name: string, content: string): string { + return `${name.length.toString(16)}:${name}${content.length.toString(16)}:${content}`; +} + +describe('getCacheFunctionSourceFile', () => { + beforeEach(() => { + vi.spyOn(process, 'cwd').mockReturnValue('/repo/apps/web'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + Reflect.deleteProperty(globalThis, NEXT_MANIFESTS_SINGLETON); + }); + + describe('JSON cache keys', () => { + it.each([ + ['the stable key shape `[buildId, functionId, args]`', stableKey], + ['the canary key shape `[functionId, args, implementationHash]`', canaryKey], + ])('resolves the source file for %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBe(LAYOUT_FILE); + }); + + it.each([ + ['an empty array', '[]'], + ['a one-element array', JSON.stringify(['build-id'])], + ['a stable-shaped key whose function id is not a string', JSON.stringify(['build-id', 42, [['arg'], {}]])], + ['a canary-shaped key whose function id is not a string', JSON.stringify([42, [['arg'], {}], 'hash'])], + ['invalid JSON that starts like an array', '[truncated'], + ])('resolves no file for %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBeUndefined(); + }); + }); + + describe('multipart cache keys', () => { + it.each([ + ['the key parts in the first field', multipartField('0', rootPayload) + multipartField('1', '{"id":"some-id"}')], + ['the key parts in a later field', multipartField('1', 'binary-ish :: data') + multipartField('0', rootPayload)], + [ + 'a preceding field whose content looks like framing', + multipartField('1', '3:abc') + multipartField('0', rootPayload), + ], + ['a preceding field with empty content', multipartField('1', '') + multipartField('0', rootPayload)], + [ + 'a preceding field with non-ASCII content counted in UTF-16 code units', + multipartField('1', 'héllo 😀 wörld') + multipartField('0', rootPayload), + ], + ])('resolves the source file for a key with %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBe(LAYOUT_FILE); + }); + + it.each([ + ['an empty key', ''], + ['a field length that is not hex', `x:0${rootPayload}`], + ['an uppercase field length (Next.js emits lowercase)', `A:aaaaaaaaaa${multipartField('0', rootPayload)}`], + ['a field length pointing past the end of the key', `1:0ffff:${rootPayload}`], + ['a key truncated inside a field', multipartField('0', rootPayload).slice(0, 10)], + ['a field name without a content field', '1:0'], + ['no field named "0"', multipartField('1', '{"id":"some-id"}')], + ['an empty field named "0"', multipartField('0', '') + multipartField('1', '{"id":"some-id"}')], + ['key parts that are a JSON object instead of an array', multipartField('0', '{"not":"an array"}')], + ])('resolves no file for a key with %s', (_label, cacheKey) => { + setManifest(LAYOUT_FILE); + + expect(getCacheFunctionSourceFile(cacheKey)).toBeUndefined(); + }); + }); + + describe('manifest lookup', () => { + it('resolves no file when the manifest singleton is missing', () => { + expect(getCacheFunctionSourceFile(stableKey)).toBeUndefined(); + }); + + it.each([ + [ + 'the manifest does not know the function id', + (): void => setManifest(LAYOUT_FILE), + JSON.stringify(['build-id', 'other-id', []]), + ], + ['the manifest entry has no filename', (): void => setManifest(undefined), stableKey], + ['the manifest filename is not a string', (): void => setManifest(42), stableKey], + ['the manifest filename is empty', (): void => setManifest(''), stableKey], + ])('resolves no file when %s', (_label, prepare, cacheKey) => { + prepare(); + + expect(getCacheFunctionSourceFile(cacheKey)).toBeUndefined(); + }); + + it('reads the edge section of the manifest on the edge runtime', () => { + vi.stubEnv('NEXT_RUNTIME', 'edge'); + setManifest(LAYOUT_FILE, 'edge'); + + expect(getCacheFunctionSourceFile(stableKey)).toBe(LAYOUT_FILE); + }); + + it('ignores the node section of the manifest on the edge runtime', () => { + vi.stubEnv('NEXT_RUNTIME', 'edge'); + setManifest(LAYOUT_FILE, 'node'); + + expect(getCacheFunctionSourceFile(stableKey)).toBeUndefined(); + }); + + it('reads the node section of the manifest when `NEXT_RUNTIME` is `nodejs`', () => { + vi.stubEnv('NEXT_RUNTIME', 'nodejs'); + setManifest(LAYOUT_FILE, 'node'); + + expect(getCacheFunctionSourceFile(stableKey)).toBe(LAYOUT_FILE); + }); + }); + + describe('project-relative paths', () => { + it.each([ + [ + 'strips a monorepo prefix that matches the cwd tail', + '/repo/dev-packages/e2e-tests/test-applications/my-app', + 'dev-packages/e2e-tests/test-applications/my-app/app/(group)/page.tsx', + 'app/(group)/page.tsx', + ], + ['keeps a `src/app` layout intact', '/repo/apps/web', 'apps/web/src/app/page.tsx', 'src/app/page.tsx'], + [ + 'is not confused by an `app` directory inside the prefix', + '/repo/packages/app', + 'packages/app/app/page.tsx', + 'app/page.tsx', + ], + ['strips the longest match when the cwd tail repeats', '/repo/a/b/a/b', 'a/b/a/b/page.tsx', 'page.tsx'], + ['splits a Windows cwd on backslashes', 'C:\\repo\\apps\\web', 'apps/web/app/page.tsx', 'app/page.tsx'], + ['ignores a trailing separator on the cwd', '/repo/apps/web/', 'apps/web/app/page.tsx', 'app/page.tsx'], + [ + 'keeps a file outside the app directory', + '/repo/apps/web', + 'apps/web/lib/cached-queries.ts', + 'lib/cached-queries.ts', + ], + [ + 'keeps the full path when the cwd does not match', + '/somewhere/else', + 'dev-packages/my-app/app/page.tsx', + 'dev-packages/my-app/app/page.tsx', + ], + ['never strips the whole path', '/repo/my-app', 'my-app', 'my-app'], + ])('%s', (_label, cwd, filename, expected) => { + vi.spyOn(process, 'cwd').mockReturnValue(cwd); + setManifest(filename); + + expect(getCacheFunctionSourceFile(stableKey)).toBe(expected); + }); + }); + + // Keys and manifest filenames captured verbatim from the `nextjs-16-cacheComponents` e2e app + // (Next 16.3 dev + prod, 16.4.0-canary.57 prod) on 2026-10-02; only the machine-specific part + // of the cwd is generalized. See `_proj-cache-components/research/cache-key-capture-howto.md`. + describe('real-world keys', () => { + const APP_PREFIX = 'dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents'; + + it.each([ + [ + 'a dev JSON key with four key parts', + '["development","80c108a36ae1b809cefcec4ecb7ede5df718f8b93a",[],"0"]', + '80c108a36ae1b809cefcec4ecb7ede5df718f8b93a', + 'app/cache/page.tsx', + ], + [ + 'a dev multipart layout key', + '1:063:["development","c0c36245c7b284678b232881c45d061cc5ce45f174",[{"params":"$@1","children":"$T"}],"0"]1:1c:{"id":"123"}', + 'c0c36245c7b284678b232881c45d061cc5ce45f174', + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + ], + [ + 'a prod JSON key (stable shape)', + '["8SLlD3QlHgVCUdTRW5hzL","c02767c889ce99f14e39a7637aae5cfabf6a939d9a",["default-id"]]', + 'c02767c889ce99f14e39a7637aae5cfabf6a939d9a', + 'app/use-cache-page/page.tsx', + ], + [ + 'a prod multipart layout key (stable shape)', + '1:069:["8SLlD3QlHgVCUdTRW5hzL","c0a941ad6a1202c965f53812b0e13854f898cba7c9",[{"params":"$@1","children":"$T"}]]1:1c:{"id":"123"}', + 'c0a941ad6a1202c965f53812b0e13854f898cba7c9', + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + ], + [ + 'a canary JSON key, third part an array rather than a hash string', + '["c02767c889ce99f14e39a7637aae5cfabf6a939d9a",["default-id"],["eWyFndmUdIGUjY7hj9qJJ"]]', + 'c02767c889ce99f14e39a7637aae5cfabf6a939d9a', + 'app/use-cache-page/page.tsx', + ], + [ + 'a canary multipart layout key', + '1:06b:["c0a941ad6a1202c965f53812b0e13854f898cba7c9",[{"params":"$@1","children":"$T"}],["eWyFndmUdIGUjY7hj9qJJ"]]1:1c:{"id":"123"}', + 'c0a941ad6a1202c965f53812b0e13854f898cba7c9', + 'app/(cached-nesting)/mixed-lifetimes/[id]/layout.tsx', + ], + [ + 'a canary route-handler key (route handlers have manifest entries on 16.4+)', + '["c06943f936da1e0a20710aae915fb68dbaa3527c74",["default-id"],["eWyFndmUdIGUjY7hj9qJJ"]]', + 'c06943f936da1e0a20710aae915fb68dbaa3527c74', + 'app/api/use-cache/route.ts', + ], + ])('resolves %s', (_label, cacheKey, functionId, appRelativePath) => { + vi.spyOn(process, 'cwd').mockReturnValue(`/repo/${APP_PREFIX}`); + setManifest(`${APP_PREFIX}/${appRelativePath}`, 'node', functionId); + + expect(getCacheFunctionSourceFile(cacheKey)).toBe(appRelativePath); + }); + }); +}); From 40e26c628cd9abf941722cd3c6bc310f0a11380b Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:11:54 +0200 Subject: [PATCH 19/73] fix(browser-utils): Handle NS_ERROR_NOT_INITIALIZED from setTimeout in Firefox (#25024) Replay calls our `setTimeout` from `@sentry/browser-utils`. When `window.setTimeout` is wrapped (by default it is, by our own `browserApiErrors` integration), that `setTimeout` comes from a sandbox iframe that is removed right away. In rare cases Firefox throws `NS_ERROR_NOT_INITIALIZED` when it is called, which shows up as an unhandled rejection from Replay's `afterSendEvent` handler. This catches the error, falls back to `window.setTimeout` and caches it. --------- Co-authored-by: Sergiy Dybskiy Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> Co-authored-by: Lukas Stracke Co-authored-by: Claude Opus 5.5 (1M context) --- .../src/getNativeImplementation.ts | 12 ++++- .../test/getNativeImplementation.test.ts | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 packages/browser-utils/test/getNativeImplementation.test.ts diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 0bb5302b12f5..402cb6bac8cc 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -119,5 +119,15 @@ export function fetch(...rest: Parameters): ReturnType): ReturnType { - return getNativeImplementation('setTimeout')(...rest); + try { + return getNativeImplementation('setTimeout')(...rest); + } catch (e) { + // The implementation retrieved from the (since removed) sandbox iframe may throw when invoked, + // e.g. Firefox throws `NS_ERROR_NOT_INITIALIZED` for timers of a detached window. + // In this case, fall back to (and cache) the window's own setTimeout. + DEBUG_BUILD && debug.warn('Could not use native setTimeout implementation, falling back to window.setTimeout: ', e); + const fallback = WINDOW.setTimeout.bind(WINDOW) as typeof WINDOW.setTimeout; + cachedImplementations.setTimeout = fallback; + return fallback(...rest); + } } diff --git a/packages/browser-utils/test/getNativeImplementation.test.ts b/packages/browser-utils/test/getNativeImplementation.test.ts new file mode 100644 index 000000000000..cb7dc59011e0 --- /dev/null +++ b/packages/browser-utils/test/getNativeImplementation.test.ts @@ -0,0 +1,53 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { clearCachedImplementation, setTimeout as sentrySetTimeout } from '../src/getNativeImplementation'; +import { WINDOW } from '../src/types'; + +describe('setTimeout', () => { + const originalSetTimeout = WINDOW.setTimeout; + + beforeEach(() => { + clearCachedImplementation('setTimeout'); + }); + + afterEach(() => { + WINDOW.setTimeout = originalSetTimeout; + clearCachedImplementation('setTimeout'); + vi.restoreAllMocks(); + }); + + it('falls back to window.setTimeout if the sandboxed iframe implementation throws', async () => { + // Simulate a wrapped (non-native) window.setTimeout, e.g. by a browser extension + const wrappedSetTimeout = vi.fn((...args: Parameters) => originalSetTimeout(...args)); + WINDOW.setTimeout = wrappedSetTimeout as unknown as typeof WINDOW.setTimeout; + + // Simulate Firefox throwing `NS_ERROR_NOT_INITIALIZED` when calling setTimeout of a detached iframe window + const detachedSetTimeout = vi.fn(() => { + const error = new Error(''); + error.name = 'NS_ERROR_NOT_INITIALIZED'; + throw error; + }); + const fakeIframe = { hidden: false, contentWindow: { setTimeout: detachedSetTimeout } }; + + const document = WINDOW.document as Document; + vi.spyOn(document, 'createElement').mockReturnValue(fakeIframe as unknown as HTMLElement); + vi.spyOn(document.head, 'appendChild').mockImplementation(node => node); + vi.spyOn(document.head, 'removeChild').mockImplementation(node => node); + + const callback = vi.fn(); + + expect(() => sentrySetTimeout(callback, 0)).not.toThrow(); + expect(detachedSetTimeout).toHaveBeenCalledTimes(1); + expect(wrappedSetTimeout).toHaveBeenCalledTimes(1); + + // Subsequent calls use the cached fallback and do not hit the broken implementation again + sentrySetTimeout(callback, 0); + expect(detachedSetTimeout).toHaveBeenCalledTimes(1); + expect(wrappedSetTimeout).toHaveBeenCalledTimes(2); + + await vi.waitFor(() => expect(callback).toHaveBeenCalledTimes(2)); + }); +}); From ad1d641833c58e8fcda5c76a22667a679324f6be Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 5 Oct 2026 14:20:53 +0200 Subject: [PATCH 20/73] fix(nextjs): Include basePath in request url of Pages Router errors (#24985) Next.js removes the `basePath` from `req.url` before running Pages Router data fetchers and API routes. So errors captured there reported a `request.url` without it, for example `http://localhost:3000/foo/bar` instead of `http://localhost:3000/base/foo/bar`. Next keeps the URL as it was originally requested in its internal request meta (`initURL`), set before the `basePath` is removed. We now take the path and query from there. The origin still comes from the request headers, because Next builds the absolute `initURL` from its own hostname and port, which can differ from the public host behind a proxy. API routes were affected the same way, so they're covered too. Fixes #24975 --------- Co-authored-by: Claude Opus 5.5 --- .../pages/api/pages-router-api-error.ts | 5 ++ .../pages/pages-router/gssp-error.tsx | 7 +++ .../tests/pages-router-request-url.test.ts | 43 ++++++++++++++ .../pages-router-instrumentation/_error.ts | 11 +--- .../wrapApiHandlerWithSentry.ts | 4 +- .../utils/pagesRouterRequestToRequestData.ts | 35 ++++++++++++ .../nextjs/src/common/utils/wrapperUtils.ts | 4 +- .../pagesRouterRequestToRequestData.test.ts | 56 +++++++++++++++++++ 8 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts create mode 100644 packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts create mode 100644 packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts new file mode 100644 index 000000000000..bc0bcf0986e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/api/pages-router-api-error.ts @@ -0,0 +1,5 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default function handler(_req: NextApiRequest, _res: NextApiResponse) { + throw new Error('Pages Router API route error with basePath'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx new file mode 100644 index 000000000000..a410f412abf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/pages/pages-router/gssp-error.tsx @@ -0,0 +1,7 @@ +export default function Page() { + return

This page should never render

; +} + +export async function getServerSideProps() { + throw new Error('Pages Router getServerSideProps error with basePath'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts new file mode 100644 index 000000000000..189f19003a5c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/tests/pages-router-request-url.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +test('Includes the basePath in the request url of getServerSideProps errors and spans', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-15-basepath', errorEvent => { + return errorEvent.exception?.values?.[0]?.value === 'Pages Router getServerSideProps error with basePath'; + }); + const segmentSpanPromise = waitForStreamedSpan('nextjs-15-basepath', span => { + return !!span.is_segment && span.name === 'GET /pages-router/gssp-error'; + }); + + await request.get('/my-app/pages-router/gssp-error?q=1'); + + const errorEvent = await errorEventPromise; + const segmentSpan = await segmentSpanPromise; + + expect(errorEvent.request).toMatchObject({ + url: 'http://localhost:3030/my-app/pages-router/gssp-error?q=1', + query_string: 'q=1', + }); + expect(segmentSpan.attributes).toMatchObject({ + 'url.full': { value: 'http://localhost:3030/my-app/pages-router/gssp-error?q=1', type: 'string' }, + }); +}); + +test('Includes the basePath in the request url of Pages Router API route errors and spans', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-15-basepath', errorEvent => { + return errorEvent.exception?.values?.[0]?.value === 'Pages Router API route error with basePath'; + }); + const segmentSpanPromise = waitForStreamedSpan('nextjs-15-basepath', span => { + return !!span.is_segment && span.name === 'GET /api/pages-router-api-error'; + }); + + await request.get('/my-app/api/pages-router-api-error'); + + const errorEvent = await errorEventPromise; + const segmentSpan = await segmentSpanPromise; + + expect(errorEvent.request?.url).toBe('http://localhost:3030/my-app/api/pages-router-api-error'); + expect(segmentSpan.attributes).toMatchObject({ + 'url.full': { value: 'http://localhost:3030/my-app/api/pages-router-api-error', type: 'string' }, + }); +}); diff --git a/packages/nextjs/src/common/pages-router-instrumentation/_error.ts b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts index 80103b42568f..9efa068e4e0d 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/_error.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts @@ -1,12 +1,7 @@ -import { - captureException, - getIsolationScope, - httpRequestToRequestData, - isAlreadyCaptured, - withScope, -} from '@sentry/core'; +import { captureException, getIsolationScope, isAlreadyCaptured, withScope } from '@sentry/core'; import type { NextPageContext } from 'next'; import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd'; +import { pagesRouterRequestToRequestData } from '../utils/pagesRouterRequestToRequestData'; type ContextOrProps = { req?: NextPageContext['req']; @@ -62,7 +57,7 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP const eventId = withScope(scope => { if (req) { - const normalizedRequest = httpRequestToRequestData(req); + const normalizedRequest = pagesRouterRequestToRequestData(req); scope.setSDKProcessingMetadata({ normalizedRequest }); } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 77343c882239..d43567a87716 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -4,7 +4,6 @@ import { getActiveSpan, getCurrentScope, getRootSpan, - httpRequestToRequestData, objectify, setCapturedScopesOnSpan, withIsolationScope, @@ -12,6 +11,7 @@ import { import type { NextApiRequest } from 'next'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; +import { pagesRouterRequestToRequestData } from '../utils/pagesRouterRequestToRequestData'; import { flushSafelyWithTimeout, waitUntil } from '../utils/responseEnd'; export type AugmentedNextApiRequest = NextApiRequest & { @@ -56,7 +56,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz return withIsolationScope(async isolationScope => { const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - isolationScope.setSDKProcessingMetadata({ normalizedRequest: httpRequestToRequestData(req) }); + isolationScope.setSDKProcessingMetadata({ normalizedRequest: pagesRouterRequestToRequestData(req) }); isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`); // We no longer create the transaction ourselves: it's the Next.js root span, which captured a different diff --git a/packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts b/packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts new file mode 100644 index 000000000000..12203f438df2 --- /dev/null +++ b/packages/nextjs/src/common/utils/pagesRouterRequestToRequestData.ts @@ -0,0 +1,35 @@ +import type { RequestEventData } from '@sentry/core'; +import { httpRequestToRequestData, parseUrl } from '@sentry/core'; +import type { IncomingMessage } from 'http'; + +const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta'); + +type RequestWithNextMeta = IncomingMessage & { + [NEXT_REQUEST_META]?: { initURL?: unknown }; +}; + +/** + * Converts a Pages Router request into request data for events. + * + * Next.js strips `basePath` from `req.url` before running pages and API routes, but keeps the URL as it was originally + * requested in its internal request meta (`initURL`). We use that one so the reported URL matches what was requested. + */ +export function pagesRouterRequestToRequestData(req: RequestWithNextMeta): RequestEventData { + const requestData = httpRequestToRequestData(req); + + const initUrl = req[NEXT_REQUEST_META]?.initURL; + // `initURL` can be absolute, but its origin is built from the Next.js server's own hostname and port rather than the + // request headers, so we only keep path and query and let the headers decide the origin like everywhere else. + const originalUrl = typeof initUrl === 'string' ? parseUrl(initUrl).relative : undefined; + if (!originalUrl || originalUrl === req.url) { + return requestData; + } + + const { url, query_string } = httpRequestToRequestData({ + url: originalUrl, + headers: req.headers, + socket: req.socket, + }); + + return { ...requestData, url, query_string }; +} diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index f340b1e69bf8..a0e6259d1404 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -6,11 +6,11 @@ import { getIsolationScope, getRootSpan, getTraceData, - httpRequestToRequestData, isThenable, } from '@sentry/core'; import type { IncomingMessage, ServerResponse } from 'http'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; +import { pagesRouterRequestToRequestData } from './pagesRouterRequestToRequestData'; /** * Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown. @@ -68,7 +68,7 @@ export function withTracedServerSideDataFetcher Pr this: unknown, ...args: Parameters ): Promise<{ data: ReturnType; sentryTrace?: string; baggage?: string }> { - const normalizedRequest = httpRequestToRequestData(req); + const normalizedRequest = pagesRouterRequestToRequestData(req); getCurrentScope().setTransactionName(`${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`); getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); diff --git a/packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts b/packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts new file mode 100644 index 000000000000..8c730d180f62 --- /dev/null +++ b/packages/nextjs/test/common/utils/pagesRouterRequestToRequestData.test.ts @@ -0,0 +1,56 @@ +import type { IncomingMessage } from 'http'; +import { describe, expect, it } from 'vitest'; +import { pagesRouterRequestToRequestData } from '../../../src/common/utils/pagesRouterRequestToRequestData'; + +function createRequest(url: string, initURL?: unknown): IncomingMessage { + const req = { + method: 'GET', + url, + headers: { host: 'example.com' }, + socket: {}, + } as unknown as IncomingMessage; + + if (initURL !== undefined) { + (req as unknown as Record)[Symbol.for('NextInternalRequestMeta')] = { initURL }; + } + + return req; +} + +describe('pagesRouterRequestToRequestData', () => { + it('uses `req.url` when there is no Next.js request meta', () => { + expect(pagesRouterRequestToRequestData(createRequest('/foo/bar?q=1'))).toMatchObject({ + url: 'http://example.com/foo/bar?q=1', + query_string: 'q=1', + method: 'GET', + }); + }); + + it('restores the basePath from a relative `initURL`', () => { + expect(pagesRouterRequestToRequestData(createRequest('/foo/bar?q=1', '/base/foo/bar?q=1'))).toMatchObject({ + url: 'http://example.com/base/foo/bar?q=1', + query_string: 'q=1', + method: 'GET', + }); + }); + + it('restores the basePath for the basePath root', () => { + expect(pagesRouterRequestToRequestData(createRequest('/', '/base'))).toMatchObject({ + url: 'http://example.com/base', + }); + }); + + it('takes only path and query from an absolute `initURL` and keeps the origin from the request headers', () => { + expect( + pagesRouterRequestToRequestData(createRequest('/foo/bar', 'http://localhost:3000/base/foo/bar')), + ).toMatchObject({ + url: 'http://example.com/base/foo/bar', + }); + }); + + it('ignores a non-string `initURL`', () => { + expect(pagesRouterRequestToRequestData(createRequest('/foo/bar', 123))).toMatchObject({ + url: 'http://example.com/foo/bar', + }); + }); +}); From 60cd46b38fb6b805c84b9e4b08d0083817db6f40 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 5 Oct 2026 15:53:42 +0200 Subject: [PATCH 21/73] ref(core): Remove `beforeSendSpan` null return warning (#24129) follow up from https://github.com/getsentry/sentry-javascript/pull/23760#discussion_r3901925320 Remove the unconditional warning when `beforeSendSpan` returns `null`, saving ~90B (compressed) / ~200B (uncompressed). The type already disallows returning null since v9. Keeps the runtime fallback to the original span for JavaScript callers, with one regression test covering the shared behavior. Co-authored-by: GPT-6 --- .../core/src/tracing/spans/beforeSendSpan.ts | 17 +------ packages/core/test/lib/client.test.ts | 45 ------------------- .../lib/tracing/spans/captureSpan.test.ts | 20 ++++----- 3 files changed, 11 insertions(+), 71 deletions(-) diff --git a/packages/core/src/tracing/spans/beforeSendSpan.ts b/packages/core/src/tracing/spans/beforeSendSpan.ts index b2fcd8261079..4acc765f19b2 100644 --- a/packages/core/src/tracing/spans/beforeSendSpan.ts +++ b/packages/core/src/tracing/spans/beforeSendSpan.ts @@ -2,7 +2,6 @@ import { DEBUG_BUILD } from '../../debug-build'; import type { BeforeSendStaticSpanCallback, BeforeSendStreamedSpanCallback } from '../../types/options'; import type { SpanJSON, StreamedSpanJSON } from '../../types/span'; import { addNonEnumerableProperty } from '../../utils/object'; -import { consoleSandbox } from '../../utils/debug-logger'; import { safeCallback } from '../../utils/safeCallback'; /** @@ -57,7 +56,6 @@ export function isStaticBeforeSendSpanCallback(callback: unknown): callback is B return !!callback && typeof callback === 'function' && '_static' in callback && !!callback._static; } -let hasShownSpanDropWarning = false; /** * Apply a user-provided beforeSendSpan callback to a span JSON. */ @@ -72,18 +70,5 @@ export function applyBeforeSendSpanCallback beforeSendSpan(span), () => span, ); - if (modifiedSpan) { - return modifiedSpan; - } - - if (!hasShownSpanDropWarning) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly or use `ignoreSpans`.', - ); - }); - hasShownSpanDropWarning = true; - } - return span; + return modifiedSpan || span; } diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index e2c3269c4929..c77bdce7d24c 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -1711,51 +1711,6 @@ describe('Client', () => { expect(loggerLogSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.'); }); - test('does not discard span and warn when returning null from `beforeSendSpan', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - - // @ts-expect-error - intentionally violating the type signature here - const beforeSendSpan = withStaticSpan(vi.fn(() => null)); - - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); - const client = new TestClient(options); - - const transaction: Event = { - transaction: '/dogs/are/great', - type: 'transaction', - spans: [ - { - description: 'first span', - span_id: '9e15bf99fbe4bc80', - start_timestamp: 1591603196.637835, - trace_id: '86f39e84263a4de99c326acab3bfe3bd', - data: {}, - status: 'ok', - }, - { - description: 'second span', - span_id: 'aa554c1f506b0783', - start_timestamp: 1591603196.637835, - trace_id: '86f39e84263a4de99c326acab3bfe3bd', - data: {}, - status: 'ok', - }, - ], - }; - client.captureEvent(transaction); - - expect(beforeSendSpan).toHaveBeenCalledTimes(3); - const capturedEvent = TestClient.instance!.event!; - expect(capturedEvent.spans).toHaveLength(2); - expect(client['_outcomes']).toEqual({}); - - expect(consoleWarnSpy).toHaveBeenCalledTimes(1); - expect(consoleWarnSpy).toHaveBeenCalledWith( - '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly or use `ignoreSpans`.', - ); - consoleWarnSpy.mockRestore(); - }); - test("doesn't throw if the `beforeSendSpan` callback throws", () => { const debugErrorSpy = vi.spyOn(debugLoggerModule.debug, 'error').mockImplementation(() => undefined); const error = new Error('beforeSendSpan is broken'); diff --git a/packages/core/test/lib/tracing/spans/captureSpan.test.ts b/packages/core/test/lib/tracing/spans/captureSpan.test.ts index 968e9a68be1b..23cdaba20cf6 100644 --- a/packages/core/test/lib/tracing/spans/captureSpan.test.ts +++ b/packages/core/test/lib/tracing/spans/captureSpan.test.ts @@ -507,8 +507,7 @@ describe('captureSpan', () => { expect(beforeSendSpan).not.toHaveBeenCalled(); }); - it('logs a warning if the beforeSendSpan callback returns null', () => { - const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + it('keeps the span if the beforeSendSpan callback returns null', () => { const beforeSendSpan = vi.fn(() => null as unknown as StreamedSpanJSON); const client = new TestClient( @@ -522,16 +521,17 @@ describe('captureSpan', () => { }), ); - const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } }); - span.end(); - - captureSpan(span, client); + const span = withScope(scope => { + scope.setClient(client); + const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } }); + span.end(); + return span; + }); - expect(consoleWarnSpy).toHaveBeenCalledWith( - '[Sentry] Returning null from `beforeSendSpan` is disallowed. To drop certain spans, configure the respective integrations directly or use `ignoreSpans`.', - ); + const serialized = captureSpan(span, client); - consoleWarnSpy.mockRestore(); + expect(serialized.span_id).toBe(span.spanContext().spanId); + expect(serialized.name).toBe('my-span'); }); it('keeps the span and logs an error if the beforeSendSpan callback throws', () => { From 7241723a271b989a28bb501cb1e1ecde4dfa9842 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Oct 2026 16:03:42 +0200 Subject: [PATCH 22/73] test(aws-serverless): Port GraphQL test to span streaming (#25056) Port the AWS serverless GraphQL integration test to default span streaming. Preserve startup and operation span checks, including the operation name, successful status, and instrumentation origin. Part of #24139 --------- Co-authored-by: GPT-6 --- .../useOperationNameForRootSpan/scenario.js | 1 - .../useOperationNameForRootSpan/test.ts | 38 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js index a37f51672bba..4023421921b5 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/scenario.js @@ -2,7 +2,6 @@ const { loggingTransport } = require('@sentry-internal/node-integration-tests'); const Sentry = require('@sentry/aws-serverless'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, integrations: [Sentry.graphqlIntegration({ useOperationNameForRootSpan: true })], diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts index 1138a4b4c6ca..723e6a9e3d95 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/graphql/useOperationNameForRootSpan/test.ts @@ -1,17 +1,6 @@ import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -const EXPECTED_TRANSCATION = { - transaction: 'Test Transaction (query GetHello)', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'query GetHello', - origin: 'auto.graphql.diagnostic_channel', - status: 'ok', - }), - ]), -}; - describe('graphqlIntegration', () => { afterAll(() => { cleanupChildProcesses(); @@ -20,8 +9,31 @@ describe('graphqlIntegration', () => { test('should use GraphQL operation name for root span if useOperationNameForRootSpan is set', async () => { await createRunner(__dirname, 'scenario.js') .ignore('event') - .expect({ transaction: { transaction: 'Test Server Start' } }) - .expect({ transaction: EXPECTED_TRANSCATION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'query GetHello', + type: 'string', + }); + const executeSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'execute'); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'GetHello', + type: 'string', + }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); From d94b677f2301e7d41adf94329d59509cf8ca43d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Mon, 5 Oct 2026 17:47:18 +0300 Subject: [PATCH 23/73] fix(cloudflare): Keep an installed OpenTelemetry async context strategy (#24995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `withSentry`, Durable Objects, Workflows and `WorkerEntrypoint` install the AsyncLocalStorage async context strategy. When an OpenTelemetry strategy is already installed, the new strategy reuses its `AsyncLocalStorage` but stores Sentry scopes in it. The OpenTelemetry context manager then reads a scope store as a context, and every later request fails with `context.getValue is not a function` (#24603). They now keep an installed OpenTelemetry strategy. This is needed for `@sentry/nextjs/cloudflare`, which installs the OpenTelemetry strategy before it calls `withSentry`, and for apps whose `register()` runs before the Worker entry. The check uses `withActiveSpan`, which only the OpenTelemetry strategy has, so no new marker is needed. The Pages plugin is unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- packages/cloudflare/src/durableobject.ts | 4 +-- .../instrumentWorkerEntrypoint.ts | 4 +-- .../src/utils/asyncContextStrategy.ts | 12 ++++++++ packages/cloudflare/src/withSentry.ts | 4 +-- packages/cloudflare/src/workflows.ts | 4 +-- .../test/utils/asyncContextStrategy.test.ts | 28 +++++++++++++++++++ 6 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 packages/cloudflare/src/utils/asyncContextStrategy.ts create mode 100644 packages/cloudflare/test/utils/asyncContextStrategy.test.ts diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index af2b4481553d..541b507a7d56 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -3,7 +3,7 @@ import { RPC } from '@sentry/conventions/op'; import { getDefaultIsolationScope, getIsolationScope, isObjectLike, startNewTrace } from '@sentry/core'; import type { DurableObject } from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import type { CloudflareOptions } from './client'; import { getInstrumented, markAsInstrumented } from './instrument'; import { instrumentDurableObjectHandlers } from './instrumentations/instrumentDurableObjectHandlers'; @@ -53,7 +53,7 @@ export function constructInstrumentedDurableObject context: InstrumentedDurableObjectContext; frameworkManagedMethods: ReadonlySet; } { - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); const options = getFinalOptions(optionsCallback(env), env); // See InstrumentedDurableObjectContext — `ctx` is widened to `any` so the concrete // `DurableObjectState` type never enters the checker's relation graph in this module. diff --git a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts index 1071d79226c6..34c9d7562ed3 100644 --- a/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts +++ b/packages/cloudflare/src/instrumentations/instrumentWorkerEntrypoint.ts @@ -1,6 +1,6 @@ import type { RpcStub, WorkerEntrypoint } from 'cloudflare:workers'; import { RPC } from '@sentry/conventions/op'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../utils/asyncContextStrategy'; import type { CloudflareOptions } from '../client'; import { markAsInstrumented } from '../instrument'; import { getFinalOptions } from '../options'; @@ -151,7 +151,7 @@ export function instrumentWorkerEntrypoint< // Set up AsyncLocalStorage strategy ONCE at instrumentation time, not per-request // This is critical - calling this per-request would create a new AsyncLocalStorage // each time, breaking scope isolation for concurrent requests - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); const InstrumentedClass = new Proxy(WorkerEntrypointClass, { construct(target, [ctx, env]) { diff --git a/packages/cloudflare/src/utils/asyncContextStrategy.ts b/packages/cloudflare/src/utils/asyncContextStrategy.ts new file mode 100644 index 000000000000..d4afe1369f4e --- /dev/null +++ b/packages/cloudflare/src/utils/asyncContextStrategy.ts @@ -0,0 +1,12 @@ +import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; + +/** + * Installs the AsyncLocalStorage strategy unless an OpenTelemetry strategy is installed. Both use the same storage, + * so a switch during a request breaks it (#24603). + */ +export function setAsyncLocalStorageAsyncContextStrategyForWorker(): void { + if (!getAsyncContextStrategy(getMainCarrier()).withActiveSpan) { + setAsyncLocalStorageAsyncContextStrategy(); + } +} diff --git a/packages/cloudflare/src/withSentry.ts b/packages/cloudflare/src/withSentry.ts index d26cc30da60c..6c991af6ed38 100644 --- a/packages/cloudflare/src/withSentry.ts +++ b/packages/cloudflare/src/withSentry.ts @@ -1,4 +1,4 @@ -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import { instrumentExportedHandlerEmail } from './instrumentations/worker/instrumentEmail'; import { instrumentExportedHandlerFetch } from './instrumentations/worker/instrumentFetch'; import { instrumentExportedHandlerQueue } from './instrumentations/worker/instrumentQueue'; @@ -37,7 +37,7 @@ export function withSentry< return instrumentWorkerEntrypoint(optionsCallback as any, handler); } - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); try { // oxlint-disable-next-line typescript/no-explicit-any diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 6043354e6427..ec065bca348e 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -19,7 +19,7 @@ import type { WorkflowStepRollbackOptions, WorkflowTimeoutDuration, } from 'cloudflare:workers'; -import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils/no-diagnostic-channels'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from './utils/asyncContextStrategy'; import type { CloudflareOptions } from './client'; import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { markAsInstrumented } from './instrument'; @@ -251,7 +251,7 @@ export function instrumentWorkflowWithSentry< get(obj, prop, receiver) { if (prop === 'run') { return async function (event: WorkflowEvent

, step: WorkflowStep): Promise { - setAsyncLocalStorageAsyncContextStrategy(); + setAsyncLocalStorageAsyncContextStrategyForWorker(); return withInvocationIsolationScope(async isolationScope => { const waitUntil = getOriginalWaitUntil(context).bind(context); diff --git a/packages/cloudflare/test/utils/asyncContextStrategy.test.ts b/packages/cloudflare/test/utils/asyncContextStrategy.test.ts new file mode 100644 index 000000000000..7f6346035a6c --- /dev/null +++ b/packages/cloudflare/test/utils/asyncContextStrategy.test.ts @@ -0,0 +1,28 @@ +import { getAsyncContextStrategy, getMainCarrier } from '@sentry/core'; +import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; +import { afterEach, describe, expect, it } from 'vitest'; +import { setAsyncLocalStorageAsyncContextStrategyForWorker } from '../../src/utils/asyncContextStrategy'; +import { resetSdk } from '../testUtils'; + +describe('setAsyncLocalStorageAsyncContextStrategyForWorker', () => { + afterEach(() => { + resetSdk(); + }); + + it('installs the AsyncLocalStorage strategy', () => { + setAsyncLocalStorageAsyncContextStrategyForWorker(); + + const strategy = getAsyncContextStrategy(getMainCarrier()); + expect(strategy.getTracingChannelBinding).toBeDefined(); + expect(strategy.withActiveSpan).toBeUndefined(); + }); + + it('keeps an installed OpenTelemetry strategy', () => { + setOpenTelemetryContextAsyncContextStrategy(); + const openTelemetryStrategy = getAsyncContextStrategy(getMainCarrier()); + + setAsyncLocalStorageAsyncContextStrategyForWorker(); + + expect(getAsyncContextStrategy(getMainCarrier())).toBe(openTelemetryStrategy); + }); +}); From a40375d9ee2d6b0ae22b4ecf5472ad9b0cb5e804 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 5 Oct 2026 16:52:03 +0200 Subject: [PATCH 24/73] ref: Reuse `isObjectLike` for object guards (#25058) Reuse the existing `isObjectLike` helper for repeated non-null object guards across SDK packages. It checks the same runtime condition and narrows unknown values, allowing redundant type assertions to be removed without changing behavior. Co-authored-by: OpenAI Codex --- packages/cloudflare/src/options.ts | 2 +- packages/deno/src/opentelemetry/tracer.ts | 4 ++-- packages/nestjs/src/helpers.ts | 17 ++++++++--------- packages/nestjs/src/setup.ts | 4 ++-- .../nestjs/test/sentry-global-filter.test.ts | 3 ++- .../routing/appRouterRoutingInstrumentation.ts | 5 +++-- .../wrapAppGetInitialPropsWithSentry.ts | 11 ++++++----- .../wrapErrorGetInitialPropsWithSentry.ts | 7 ++++--- .../wrapGetInitialPropsWithSentry.ts | 7 ++++--- .../wrapGetServerSidePropsWithSentry.ts | 3 ++- packages/react-router/src/client/utils.ts | 9 +++++---- packages/remix/src/v3/server/errorFilter.ts | 6 +++--- packages/remix/test/v3/errorFilter.test.ts | 6 +++++- .../hono/defaultShouldHandleError.ts | 6 ++++-- .../src/integrations/kafkajs/spans.ts | 3 ++- .../server-utils/src/integrations/langgraph.ts | 4 ++-- .../server-utils/src/integrations/mastra.ts | 2 +- 17 files changed, 56 insertions(+), 43 deletions(-) diff --git a/packages/cloudflare/src/options.ts b/packages/cloudflare/src/options.ts index a90a236e4bf1..0d65340b306b 100644 --- a/packages/cloudflare/src/options.ts +++ b/packages/cloudflare/src/options.ts @@ -39,7 +39,7 @@ function getEnvVar>(env: unknown, varName: key * @returns The final options. */ export function getFinalOptions(userOptions: CloudflareOptions = {}, env: unknown): CloudflareOptions { - if (typeof env !== 'object' || env === null) { + if (!isObjectLike(env)) { return userOptions; } diff --git a/packages/deno/src/opentelemetry/tracer.ts b/packages/deno/src/opentelemetry/tracer.ts index e9846b994e45..ce3516feece4 100644 --- a/packages/deno/src/opentelemetry/tracer.ts +++ b/packages/deno/src/opentelemetry/tracer.ts @@ -1,7 +1,7 @@ import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; -import { startInactiveSpan, startSpanManual } from '@sentry/core'; +import { isObjectLike, startInactiveSpan, startSpanManual } from '@sentry/core'; /** * Set up a mock OTEL tracer to allow inter-op with OpenTelemetry emitted spans. @@ -58,7 +58,7 @@ class SentryDenoTracer implements Tracer { context?: unknown, fn?: F, ): ReturnType { - const opts = (typeof options === 'object' && options !== null ? options : {}) as SpanOptions; + const opts = (isObjectLike(options) ? options : {}) as SpanOptions; const spanOpts = { ...opts, diff --git a/packages/nestjs/src/helpers.ts b/packages/nestjs/src/helpers.ts index 58d03dda019f..56f9a5704b32 100644 --- a/packages/nestjs/src/helpers.ts +++ b/packages/nestjs/src/helpers.ts @@ -1,3 +1,5 @@ +import { isObjectLike } from '@sentry/core'; + /** * Determines if the exception is an expected NestJS control flow error. * - HttpException have getStatus and getResponse methods: https://github.com/nestjs/nest/blob/master/packages/microservices/exceptions/rpc-exception.ts @@ -10,17 +12,15 @@ * @returns `true` if the exception is expected and should not be reported to Sentry, otherwise `false`. */ export function isExpectedError(exception: unknown): boolean { - if (typeof exception !== 'object' || exception === null) { + if (!isObjectLike(exception)) { return false; } - const ex = exception as Record; - // HttpException if ( - typeof ex.getStatus === 'function' && - typeof ex.getResponse === 'function' && - typeof ex.initMessage === 'function' + typeof exception.getStatus === 'function' && + typeof exception.getResponse === 'function' && + typeof exception.initMessage === 'function' ) { return true; } @@ -39,10 +39,9 @@ export function isExpectedError(exception: unknown): boolean { * We use duck-typing to avoid importing from `@nestjs/websockets` or `@nestjs/microservices`. */ export function isWsOrRpcException(exception: unknown): boolean { - if (typeof exception !== 'object' || exception === null) { + if (!isObjectLike(exception)) { return false; } - const ex = exception as Record; - return typeof ex.getError === 'function' && typeof ex.initMessage === 'function'; + return typeof exception.getError === 'function' && typeof exception.initMessage === 'function'; } diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index b646806532b4..a8e56a9d8a66 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -8,7 +8,7 @@ import type { } from '@nestjs/common'; import { Catch, Global, HttpException, Injectable, Logger, Module } from '@nestjs/common'; import { APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core'; -import { captureException, debug, getDefaultIsolationScope, getIsolationScope } from '@sentry/core'; +import { captureException, debug, getDefaultIsolationScope, getIsolationScope, isObjectLike } from '@sentry/core'; import type { Observable } from 'rxjs'; import { isExpectedError, isWsOrRpcException } from './helpers'; @@ -166,7 +166,7 @@ class SentryGlobalFilter extends BaseExceptionFilter { if (isWsOrRpcException(exception)) { const result = (exception as { getError: () => unknown }).getError(); - const response = typeof result === 'object' && result !== null ? result : { status: 'error', message: result }; + const response = isObjectLike(result) ? result : { status: 'error', message: result }; client.emit?.('exception', response); return; } diff --git a/packages/nestjs/test/sentry-global-filter.test.ts b/packages/nestjs/test/sentry-global-filter.test.ts index d5f772992854..90c340aa2992 100644 --- a/packages/nestjs/test/sentry-global-filter.test.ts +++ b/packages/nestjs/test/sentry-global-filter.test.ts @@ -11,7 +11,8 @@ vi.mock('../src/helpers', () => ({ isWsOrRpcException: vi.fn(), })); -vi.mock('@sentry/core', () => ({ +vi.mock('@sentry/core', async importOriginal => ({ + isObjectLike: (await importOriginal()).isObjectLike, captureException: vi.fn().mockReturnValue('mock-event-id'), getIsolationScope: vi.fn(), getDefaultIsolationScope: vi.fn(), diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 26133c1d9747..4b7e9b1c7ae5 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -2,6 +2,7 @@ import type { Client, Span } from '@sentry/core'; import { GLOBAL_OBJ, hasSpanStreamingEnabled, + isObjectLike, NAVIGATION_SPAN_NAME_FALLBACK, PAGELOAD_SPAN_NAME_FALLBACK, filterCollectedUrl, @@ -250,8 +251,8 @@ export function appRouterInstrumentNavigation(client: Client): void { if (globalValue) { GLOBAL_OBJ_WITH_NEXT_ROUTER.next = new Proxy(globalValue, { set(target, p, newValue) { - if (p === 'router' && typeof newValue === 'object' && newValue !== null) { - patchRouter(client, newValue, currentRouterPatchingNavigationSpanRef); + if (p === 'router' && isObjectLike(newValue)) { + patchRouter(client, newValue as unknown as NextRouter, currentRouterPatchingNavigationSpanRef); } // @ts-expect-error we cannot possibly type this diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts index 02c54e76b3d6..7fda6a1da56e 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type App from 'next/app'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -45,23 +46,23 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI baggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); - if (typeof appGetInitialProps === 'object' && appGetInitialProps !== null) { + if (isObjectLike(appGetInitialProps)) { // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. - if (!(appGetInitialProps as Record).pageProps) { - (appGetInitialProps as Record).pageProps = {}; + if (!appGetInitialProps.pageProps) { + appGetInitialProps.pageProps = {}; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { - (appGetInitialProps as { pageProps: Record }).pageProps._sentryTraceData = sentryTrace; + (appGetInitialProps.pageProps as Record)._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (appGetInitialProps as { pageProps: Record }).pageProps._sentryBaggage = baggage; + (appGetInitialProps.pageProps as Record)._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts index 209ca486db15..1346b78ffd25 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; import { isBuild } from '../utils/isBuild'; @@ -47,15 +48,15 @@ export function wrapErrorGetInitialPropsWithSentry( sentryTrace?: string; } = await tracedGetInitialProps.apply(thisArg, args); - if (typeof errorGetInitialProps === 'object' && errorGetInitialProps !== null) { + if (isObjectLike(errorGetInitialProps)) { if (sentryTrace) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) - (errorGetInitialProps as Record)._sentryTraceData = sentryTrace; + errorGetInitialProps._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (errorGetInitialProps as Record)._sentryBaggage = baggage; + errorGetInitialProps._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts index f597692820b8..82fc76343785 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { NextPage } from 'next'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -43,15 +44,15 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro sentryTrace?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. - if (typeof initialProps === 'object' && initialProps !== null) { + if (isObjectLike(initialProps)) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { - (initialProps as Record)._sentryTraceData = sentryTrace; + initialProps._sentryTraceData = sentryTrace; } // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (baggage) { - (initialProps as Record)._sentryBaggage = baggage; + initialProps._sentryBaggage = baggage; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts index 5512c6204410..f99faa528f91 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts @@ -1,3 +1,4 @@ +import { isObjectLike } from '@sentry/core'; import type { GetServerSideProps } from 'next'; import { isBuild } from '../utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; @@ -39,7 +40,7 @@ export function wrapGetServerSidePropsWithSentry( sentryTrace?: string; } = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType); - if (typeof serverSideProps === 'object' && serverSideProps !== null && 'props' in serverSideProps) { + if (isObjectLike(serverSideProps) && 'props' in serverSideProps) { // The Next.js serializer throws on undefined values so we need to guard for it (#12102) if (sentryTrace) { (serverSideProps.props as Record)._sentryTraceData = sentryTrace; diff --git a/packages/react-router/src/client/utils.ts b/packages/react-router/src/client/utils.ts index 94044c096c94..eeb029f44878 100644 --- a/packages/react-router/src/client/utils.ts +++ b/packages/react-router/src/client/utils.ts @@ -4,6 +4,7 @@ import { getClient, GLOBAL_OBJ, hasSpanStreamingEnabled, + isObjectLike, NAVIGATION_SPAN_NAME_FALLBACK, filterCollectedUrl, } from '@sentry/core'; @@ -24,13 +25,13 @@ const GLOBAL_OBJ_WITH_DATA_ROUTER = GLOBAL_OBJ as typeof GLOBAL_OBJ & { * to avoid "[object Object]" transaction names. */ export function resolveNavigateArg(target: unknown): string { - if (typeof target !== 'object' || target === null) { + if (!isObjectLike(target)) { // string or number return String(target); } // Object `to` with pathname - const pathname = (target as Record).pathname; + const pathname = target.pathname; if (typeof pathname === 'string') { return pathname || '/'; } @@ -46,12 +47,12 @@ export function resolveNavigateArg(target: unknown): string { * destination the user is navigating to, including any query string. */ export function resolveNavigateUrl(target: unknown): string { - if (typeof target !== 'object' || target === null) { + if (!isObjectLike(target)) { // string or number return String(target); } - const { pathname, search, hash } = target as Record; + const { pathname, search, hash } = target; const path = typeof pathname === 'string' && pathname !== '' ? pathname : WINDOW.location?.pathname || '/'; return `${path}${typeof search === 'string' ? search : ''}${typeof hash === 'string' ? hash : ''}`; diff --git a/packages/remix/src/v3/server/errorFilter.ts b/packages/remix/src/v3/server/errorFilter.ts index 9e98a2874a27..5a771746f8b0 100644 --- a/packages/remix/src/v3/server/errorFilter.ts +++ b/packages/remix/src/v3/server/errorFilter.ts @@ -1,4 +1,4 @@ -import { captureException } from '@sentry/core'; +import { captureException, isObjectLike } from '@sentry/core'; /** Decides whether a thrown value should become a Sentry issue. */ export type ShouldHandleError = (error: unknown) => boolean; @@ -8,11 +8,11 @@ export type ShouldHandleError = (error: unknown) => boolean; * outcome, not a fault, and the request still produces a span. */ export function defaultShouldHandleError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) { + if (!isObjectLike(error)) { return true; } - const status = (error as { status?: unknown }).status; + const status = error.status; return !(typeof status === 'number' && status >= 300 && status < 500); } diff --git a/packages/remix/test/v3/errorFilter.test.ts b/packages/remix/test/v3/errorFilter.test.ts index f307f8bdd4e4..ab17dbedadea 100644 --- a/packages/remix/test/v3/errorFilter.test.ts +++ b/packages/remix/test/v3/errorFilter.test.ts @@ -1,7 +1,11 @@ +import type * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const captureException = vi.fn(); -vi.mock('@sentry/core', () => ({ captureException: (...args: unknown[]) => captureException(...args) })); +vi.mock('@sentry/core', async importOriginal => ({ + isObjectLike: (await importOriginal()).isObjectLike, + captureException: (...args: unknown[]) => captureException(...args), +})); const { captureRequestError, defaultShouldHandleError, isRequestAbort, setShouldHandleError } = await import('../../src/v3/server/errorFilter'); diff --git a/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts b/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts index 71eab3bbb3fd..c166faea6384 100644 --- a/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts +++ b/packages/server-utils/src/integrations/hono/defaultShouldHandleError.ts @@ -1,3 +1,5 @@ +import { isObjectLike } from '@sentry/core'; + /** * Default implementation of the `shouldHandleError` callback. * @@ -9,11 +11,11 @@ * Hono's `HTTPException`, third-party middleware errors, and custom error subclasses. */ export function defaultShouldHandleError(error: unknown): boolean { - if (typeof error !== 'object' || error === null) { + if (!isObjectLike(error)) { return true; } - const status = (error as { status?: unknown }).status; + const status = error.status; return !(typeof status === 'number' && status >= 300 && status < 500); } diff --git a/packages/server-utils/src/integrations/kafkajs/spans.ts b/packages/server-utils/src/integrations/kafkajs/spans.ts index fdae5a0d46bd..3ed0ca5a84bd 100644 --- a/packages/server-utils/src/integrations/kafkajs/spans.ts +++ b/packages/server-utils/src/integrations/kafkajs/spans.ts @@ -26,6 +26,7 @@ import { getClient, getTraceData, hasSpanStreamingEnabled, + isObjectLike, propagationContextFromHeaders, SPAN_STATUS_ERROR, startInactiveSpan, @@ -183,7 +184,7 @@ export function applyErrorToSpans(spans: Span[], reason: unknown): void { let errorType: string = ERROR_TYPE_VALUE_OTHER; if (typeof reason === 'string' || reason === undefined) { errorMessage = reason; - } else if (typeof reason === 'object' && reason !== null && Object.prototype.hasOwnProperty.call(reason, 'message')) { + } else if (isObjectLike(reason) && Object.prototype.hasOwnProperty.call(reason, 'message')) { errorMessage = (reason as { message?: string }).message; errorType = (reason as { constructor: { name: string } }).constructor.name; } diff --git a/packages/server-utils/src/integrations/langgraph.ts b/packages/server-utils/src/integrations/langgraph.ts index a6b9f6f21541..5a41a8296aa3 100644 --- a/packages/server-utils/src/integrations/langgraph.ts +++ b/packages/server-utils/src/integrations/langgraph.ts @@ -1,6 +1,6 @@ import * as diagnosticsChannel from '../utils/diagnosticsChannel'; import type { IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration } from '@sentry/core'; +import { debug, defineIntegration, isObjectLike } from '@sentry/core'; import { resolveAIRecordingOptions } from '../ai/core/utils'; import { createLangChainCallbackHandler } from '../ai/langchain'; import { instrumentCompiledGraphInvoke } from '../ai/langgraph'; @@ -95,7 +95,7 @@ function instrumentLanggraph(options: LangGraphOptions): void { function getFirstArgObject(args: unknown[] | undefined): Record | undefined { const first = (args ?? [])[0]; - return typeof first === 'object' && first !== null ? (first as Record) : undefined; + return isObjectLike(first) ? first : undefined; } /** diff --git a/packages/server-utils/src/integrations/mastra.ts b/packages/server-utils/src/integrations/mastra.ts index cfd1c5c70995..9856ef51651d 100644 --- a/packages/server-utils/src/integrations/mastra.ts +++ b/packages/server-utils/src/integrations/mastra.ts @@ -176,7 +176,7 @@ function instrumentExecuteWithContext(): void { } function attachExporter(instance: unknown, options: MastraOptions): void { - if (typeof instance !== 'object' || instance === null || registered.has(instance)) { + if (!isObjectLike(instance) || registered.has(instance)) { return; } From e741ce823cca203a3c6d721056384290be1ad459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Mon, 5 Oct 2026 18:39:43 +0300 Subject: [PATCH 25/73] fix(sveltekit): Export consoleLoggingIntegration from worker entry (#25061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The public types of `@sentry/sveltekit` include `consoleLoggingIntegration` from the Node entry. But the worker entry, which Cloudflare selects through the `worker` and `workerd` conditions, did not re-export it from `@sentry/cloudflare`. So `Sentry.consoleLoggingIntegration()` was `undefined` on Workers. This adds only that one export. Other Node-typed exports are also missing from the worker entry. We will handle them with a dedicated Cloudflare entry point, which can also fix the types (#24841). Fixes #24965 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- packages/sveltekit/src/worker/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index c7b6c5b11d54..1ab26de90d38 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -26,6 +26,7 @@ export { captureFeedback, captureMessage, close, + consoleLoggingIntegration, continueTrace, createTransport, dedupeIntegration, From 637aa3d3509ae9c27622e2002cc42f2b8a235f20 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 5 Oct 2026 19:04:32 +0200 Subject: [PATCH 26/73] test(ember): Pin embroider packages away from broken releases (#25066) Embroider published new patch versions of vite, macros, core and compat today with almost no files in the tarballs, which broke every ember e2e app, fixed by overriding just those versions back to the previous ones in the test apps. The overrides are scoped to the broken versions, so they stop applying once a fixed release is out and can be removed then. --- .../test-applications/ember-classic/package.json | 5 +++++ .../test-applications/ember-embroider/package.json | 5 +++++ .../test-applications/ember-strict-resolver/package.json | 8 ++++++++ .../e2e-tests/test-applications/ember-vite/package.json | 8 ++++++++ 4 files changed, 26 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/package.json b/dev-packages/e2e-tests/test-applications/ember-classic/package.json index f5cfb2795c43..87da5f5f5571 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-classic/package.json @@ -81,6 +81,11 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/macros@1.21.2": "1.21.1" + } + }, "volta": { "extends": "../../package.json" } diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json index 0653d743b450..67d7d462d9b2 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/package.json @@ -66,6 +66,11 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/macros@1.21.2": "1.21.1" + } + }, "volta": { "extends": "../../package.json" } diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json index 7acab343c96c..d1230979cc65 100644 --- a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json @@ -42,6 +42,14 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/vite@1.7.14": "1.7.13", + "@embroider/macros@1.21.2": "1.21.1", + "@embroider/core@4.6.8": "4.6.7", + "@embroider/compat@4.1.26": "4.1.25" + } + }, "volta": { "extends": "../../package.json" }, diff --git a/dev-packages/e2e-tests/test-applications/ember-vite/package.json b/dev-packages/e2e-tests/test-applications/ember-vite/package.json index 7106f8aa2a00..c1bcfd114fa5 100644 --- a/dev-packages/e2e-tests/test-applications/ember-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-vite/package.json @@ -67,6 +67,14 @@ "ember": { "edition": "octane" }, + "pnpm": { + "overrides": { + "@embroider/vite@1.7.14": "1.7.13", + "@embroider/macros@1.21.2": "1.21.1", + "@embroider/core@4.6.8": "4.6.7", + "@embroider/compat@4.1.26": "4.1.25" + } + }, "volta": { "extends": "../../package.json" } From 21ba35d6efdd940e162ec17c5f2625d880f97eda Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 5 Oct 2026 19:17:53 +0200 Subject: [PATCH 27/73] chore(deps): Bump web-vitals to 6.2.3 (#25062) We were on web-vitals 6.1.1, so I bumped it to the latest 6.2.3 to pick up the upstream fixes. **Changelog** - Fixes an INP clean-up memory leak and caps pending LoAFs to avoid another leak ([#799](https://github.com/GoogleChrome/web-vitals/pull/799), [#796](https://github.com/GoogleChrome/web-vitals/pull/796)) - Fixes negative `resourceLoadDuration` in LCP attribution and negative `inputDelay` in INP attribution ([#803](https://github.com/GoogleChrome/web-vitals/pull/803), [#789](https://github.com/GoogleChrome/web-vitals/pull/789)) - Stops a spurious CLS report of 0 after a bfcache restore ([#786](https://github.com/GoogleChrome/web-vitals/pull/786)) - Guards `supportedEntryTypes` for older browsers ([#787](https://github.com/GoogleChrome/web-vitals/pull/787)) --- packages/browser-utils/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 50da891bf853..a5ffcf04b7cb 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -35,7 +35,7 @@ "dependencies": { "@sentry/core": "11.4.0", "@sentry/conventions": "^0.25.0", - "web-vitals": "^6.1.1" + "web-vitals": "^6.2.3" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/yarn.lock b/yarn.lock index ef776d7290d0..6321817fad73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27743,10 +27743,10 @@ web-streams-polyfill@^3.1.1: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== -web-vitals@^6.1.1: - version "6.1.1" - resolved "https://sfw.security.sentry.io/npm/web-vitals/-/web-vitals-6.1.1.tgz#855ba63ed40847c55e061ac13f55ee5f6435b723" - integrity sha512-r93gKnR6ZC2O8F3JrS0AAGIQ0v0OhXuyg4DAj2oG7K+GPkwXfUSc3ZcWeu50AsV5yIc6xxnTjERipJ6EvSw2vg== +web-vitals@^6.2.3: + version "6.2.3" + resolved "https://sfw.security.sentry.io/npm/web-vitals/-/web-vitals-6.2.3.tgz#8581188b2a46875d04b9536ab06b352e3a0468b8" + integrity sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A== webidl-conversions@^3.0.0: version "3.0.1" From f469f76907c3929c2fea026e2165d9471c15bd9c Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 5 Oct 2026 14:00:55 -0700 Subject: [PATCH 28/73] fix(server-utils): Record Vercel AI `experimental_telemetry.metadata` on gen_ai spans (#24721) v10 got `vercel.ai.telemetry.metadata.` attributes from the AI SDK's own OTel spans. In v11, `ai` 4 to 6 go through the orchestrion subscriber, which never read `metadata`, so v11 dropped it. The orchestrion subscriber now passes the metadata to the channel subscriber, which sets it as span attributes when each span starts. The `invoke_agent`, `generate_content` and `embeddings` spans get the attributes, and `execute_tool` spans don't. This matches the AI SDK, which leaves metadata off its `ai.toolCall` spans. Values are cast to string, as discussed in the linked sentry-conventions PR. If this causes a problem we can revisit, but all extant examples found in the wild are strings already, so it is likely safe, and extremely cheap. fixes JS-3800 fixes #24720 Re: #24706 Re: getsentry/sentry-conventions#662 Co-authored-by: Claude Opus 5.5 (1M context) --- .../scenario-telemetry-metadata.mjs | 82 +++++++++++++++++++ .../vercelai/span-streaming-v6/test.ts | 43 ++++++++++ .../vercel-ai/vercel-ai-dc-subscriber.ts | 20 ++++- .../vercel-ai-orchestrion-subscriber.ts | 8 ++ .../vercel-ai/telemetry-metadata.test.ts | 61 ++++++++++++++ 5 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs create mode 100644 packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs new file mode 100644 index 000000000000..303a577f8ef0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/scenario-telemetry-metadata.mjs @@ -0,0 +1,82 @@ +import * as Sentry from '@sentry/node'; +import { embed, embedMany, generateText, streamText } from 'ai'; +import { MockEmbeddingModelV3, MockLanguageModelV3, simulateReadableStream } from 'ai/test'; + +const usage = { + inputTokens: { total: 10, noCache: 10, cached: 0 }, + outputTokens: { total: 20, noCache: 20, cached: 0 }, + totalTokens: { total: 30, noCache: 30, cached: 0 }, +}; + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + await generateText({ + experimental_telemetry: { + isEnabled: true, + functionId: 'support-chat', + metadata: { requestId: 'req_generate', tenantId: 'acme', attempt: 2 }, + }, + model: new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage, + content: [{ type: 'text', text: 'Hello!' }], + warnings: [], + }), + }), + prompt: 'Hi', + }); + + const result = streamText({ + experimental_telemetry: { + isEnabled: true, + functionId: 'support-stream', + metadata: { requestId: 'req_stream', tenantId: 'acme' }, + }, + model: new MockLanguageModelV3({ + doStream: async () => ({ + stream: simulateReadableStream({ + chunks: [ + { type: 'stream-start', warnings: [] }, + { type: 'text-start', id: '0' }, + { type: 'text-delta', id: '0', delta: 'Hello!' }, + { type: 'text-end', id: '0' }, + { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage }, + ], + }), + }), + }), + prompt: 'Hi', + }); + for await (const _part of result.fullStream) { + void _part; + } + + await embed({ + experimental_telemetry: { isEnabled: true, metadata: { requestId: 'req_embed', tenantId: 'acme' } }, + model: new MockEmbeddingModelV3({ + doEmbed: async () => ({ embeddings: [[0.1, 0.2, 0.3]], usage: { tokens: 10 } }), + }), + value: 'Embed me', + }); + + await embedMany({ + experimental_telemetry: { isEnabled: true, metadata: { requestId: 'req_embed_many', tenantId: 'acme' } }, + model: new MockEmbeddingModelV3({ + maxEmbeddingsPerCall: 5, + doEmbed: async () => ({ + embeddings: [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + ], + usage: { tokens: 20 }, + }), + }), + values: ['First', 'Second'], + }); + }); + + await Sentry.flush(2000); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts index db4707374c4e..68c88b131783 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/span-streaming-v6/test.ts @@ -305,6 +305,49 @@ describe('Vercel AI integration (streaming, v6)', () => { }, ); + function spanWithMetadata(name: string, op: string, requestId: string, extra: Record = {}) { + return expect.objectContaining({ + name, + attributes: expect.objectContaining({ + 'vercel.ai.telemetry.metadata.requestId': attr(requestId), + 'vercel.ai.telemetry.metadata.tenantId': attr('acme'), + ...extra, + [SENTRY_OP]: attr(op), + }), + }); + } + + const EXPECTED_SPANS_TELEMETRY_METADATA = { + items: expect.arrayContaining([ + spanWithMetadata('invoke_agent support-chat', 'gen_ai.invoke_agent', 'req_generate', { + 'vercel.ai.telemetry.metadata.attempt': { type: 'string', value: '2' }, + }), + spanWithMetadata('generate_content mock-model-id', 'gen_ai.generate_content', 'req_generate', { + 'vercel.ai.telemetry.metadata.attempt': { type: 'string', value: '2' }, + }), + spanWithMetadata('invoke_agent support-stream', 'gen_ai.invoke_agent', 'req_stream'), + spanWithMetadata('generate_content mock-model-id', 'gen_ai.generate_content', 'req_stream'), + spanWithMetadata('embeddings mock-model-id', 'gen_ai.embeddings', 'req_embed'), + spanWithMetadata('embeddings mock-model-id', 'gen_ai.embeddings', 'req_embed_many'), + ]), + }; + + createEsmAndCjsTests( + __dirname, + 'scenario-telemetry-metadata.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('records experimental_telemetry.metadata as span attributes', async () => { + await createRunner().expect({ span: EXPECTED_SPANS_TELEMETRY_METADATA }).start().completed(); + }); + }, + { + additionalDependencies: { + ai: '^6.0.0', + }, + }, + ); + createEsmAndCjsTests( __dirname, 'scenario-error-in-tool.mjs', diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index d1b420635876..2ea07e96251c 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -86,6 +86,7 @@ type GenAiOperation = keyof typeof GEN_AI_OPERATION_SPAN_OPS; const VERCEL_AI_OPERATION_ID_ATTRIBUTE = 'vercel.ai.operationId'; const VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE = 'vercel.ai.model.provider'; const VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE = 'vercel.ai.settings.maxRetries'; +const VERCEL_AI_TELEMETRY_METADATA_ATTRIBUTE_PREFIX = 'vercel.ai.telemetry.metadata.'; // Tracks the top-level operationId (and whether it streams) per `callId` so a model-call span can // name its `doGenerate`/`doStream` operation the same way the OTel integration does. `isStream` is @@ -417,8 +418,9 @@ export function createSpanFromMessage( recordToolDescriptions(callId, event.tools); } - const baseAttributes: Record = { + const baseAttributes: SpanAttributes = { [SENTRY_ORIGIN]: ORIGIN, + ...telemetryMetadataAttributes(event.telemetryMetadata), ...(provider ? { [GEN_AI_PROVIDER_NAME]: provider, [VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE]: provider } : {}), ...(modelId ? { [GEN_AI_REQUEST_MODEL]: modelId } : {}), ...(maxRetries !== undefined ? { [VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE]: maxRetries } : {}), @@ -466,6 +468,22 @@ export function createSpanFromMessage( } } +/** + * `experimental_telemetry.metadata` (`ai` <= 6) as `vercel.ai.telemetry.metadata.`, the names the OTel + * integration produced from the SDK's `ai.telemetry.metadata.*`. Only the orchestrion adapter sets + * `event.telemetryMetadata`; `ai` 7 has no `telemetry.metadata`. + */ +function telemetryMetadataAttributes(metadata: unknown): Record { + const attributes: Record = {}; + if (isObjectLike(metadata)) { + for (const [key, value] of Object.entries(metadata)) { + if (value === null || value === undefined) continue; + attributes[`${VERCEL_AI_TELEMETRY_METADATA_ATTRIBUTE_PREFIX}${key}`] = String(value); + } + } + return attributes; +} + /** Start a `gen_ai.` span named ` ` (or just `` when no suffix). */ function startGenAiSpan(operation: GenAiOperation, suffix: string | undefined, attributes: SpanAttributes): Span { return startInactiveSpan({ diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index ea64ae6aacd9..40f076f54bec 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -107,6 +107,9 @@ const callIdBySpan = new WeakMap(); // child `generate_content` span (whose event would fall back to the global default). v7's channel forwards // these flags on every event, so this keeps v6 identical. const recordingBySpan = new WeakMap>(); +// The operation's `experimental_telemetry.metadata`, keyed by its span, so the model-call span records it +// too. The OTel integration put it on both, but not on tool-call spans. +const telemetryMetadataBySpan = new WeakMap(); interface OperationErrorInfo { // The span active when the operation was invoked (its call site, e.g. the enclosing request/`main` span). // When an operation's error bubbles out unhandled it reaches the global handler outside any span, so — as @@ -163,6 +166,7 @@ export function subscribeVercelAiOrchestrionChannels( ...modelFields(callOptions.model), maxRetries: callOptions.maxRetries, value: callOptions.value, + telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, }), @@ -180,6 +184,7 @@ export function subscribeVercelAiOrchestrionChannels( ...modelFields(callOptions.model), maxRetries: callOptions.maxRetries, values: callOptions.values, + telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, }), @@ -263,6 +268,7 @@ function bindOperation( callIdBySpan.set(span, callId); } recordingBySpan.set(span, recording(telemetry)); + telemetryMetadataBySpan.set(span, telemetry.metadata); // v5 has no `executeToolCall` channel, so patch each tool's `execute` to emit the tool-call span. // Inert on v6 (guarded inside `patchToolExecute` when the parent is `executeToolCall`'s own span). if (isObjectLike(callOptions.tools)) { @@ -490,6 +496,7 @@ function patchModelMethod( // top-level `tools` array. Reading both keeps `tool.definitions` populated on the model-call span. tools: callArgs.tools ?? (isObjectLike(callArgs.mode) ? callArgs.mode.tools : undefined), messages: callArgs.prompt, + telemetryMetadata: telemetryMetadataBySpan.get(parent), // Inherit the enclosing operation's per-call recording flags so inputs/tools/outputs are recorded on // the model-call span whenever they are on the parent `invoke_agent` span. ...recordingBySpan.get(parent), @@ -669,6 +676,7 @@ function buildTextMessage(type: 'generateText' | 'streamText' | 'generateObject' // Normalize to the message-array shape the shared core (and v7's channel) expects: a bare string // `prompt` becomes a single user message, matching the SDK's own normalization. messages: normalizePromptMessages(options), + telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, }); diff --git a/packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts b/packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts new file mode 100644 index 000000000000..e47c2f3d272a --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/telemetry-metadata.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createSpanFromMessage } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +describe('Vercel AI telemetry metadata', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + it('records metadata values as strings and skips null and undefined', () => { + const endedSpans = setupClient(); + const span = createSpanFromMessage( + { + type: 'generateText', + event: { + telemetryMetadata: { + requestId: 'req_1', + attempt: 2, + cached: false, + tags: ['a', 'b'], + empty: null, + missing: undefined, + }, + }, + } as Parameters[0], + {} as Parameters[1], + ); + span?.end(); + + const data = spanToStaticSpanJSON(endedSpans[0]!).data; + expect(data).toMatchObject({ + 'vercel.ai.telemetry.metadata.requestId': 'req_1', + 'vercel.ai.telemetry.metadata.attempt': '2', + 'vercel.ai.telemetry.metadata.cached': 'false', + 'vercel.ai.telemetry.metadata.tags': 'a,b', + }); + expect(data).not.toHaveProperty('vercel.ai.telemetry.metadata.empty'); + expect(data).not.toHaveProperty('vercel.ai.telemetry.metadata.missing'); + }); +}); From 161bd9426dd4121587612ed9e6d3b952b3429971 Mon Sep 17 00:00:00 2001 From: Sergiy Dybskiy Date: Mon, 5 Oct 2026 17:08:20 -0400 Subject: [PATCH 29/73] fix(node): Flush telemetry before Vercel functions are suspended (#24910) On Vercel, a function can be suspended as soon as it sends the response. Telemetry that is still buffered (spans, logs, metrics, errors) then arrives late or never. When a response finishes, `@sentry/node` now uses `waitUntil` to keep the function alive. It waits until the response closes and the request's root span ends (2 s max), then flushes. It works with or without tracing. Some frameworks end the root span shortly after the response closes, so the flush waits for it. Tested with unit tests and an integration test that fails without the change, and confirmed on a Vercel deployment. Closes #24909 Refs #23087 --------- Co-authored-by: Claude --- .../node-suites/excludes.ts | 3 +- .../suites/vercel/keep-alive/scenario.ts | 124 +++++++++++++ .../suites/vercel/keep-alive/test.ts | 110 ++++++++++++ packages/node/src/sdk/index.ts | 8 +- packages/node/src/sdk/vercel.ts | 86 +++++++++ packages/node/test/sdk/vercel.test.ts | 170 ++++++++++++++++++ 6 files changed, 494 insertions(+), 7 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts create mode 100644 packages/node/src/sdk/vercel.ts create mode 100644 packages/node/test/sdk/vercel.test.ts diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 20124be5a264..9705eff5ff1e 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -2,7 +2,7 @@ // fails on Bun is skipped with `test.skipIf` on `RUNTIME` in the Node suite, not listed here. // Node-only features: ANR and native thread watchdogs, child processes, the AWS Lambda Node runtime, -// and `node:sqlite`, which `flue` needs. +// `node:sqlite`, which `flue` needs, and the Vercel keep-alive, which needs `http.server.response.finish`. const NODE_ONLY = [ 'suites/anr/test.ts', 'suites/aws-serverless/**', @@ -10,6 +10,7 @@ const NODE_ONLY = [ 'suites/child-process/test.ts', 'suites/thread-blocked-native/test.ts', 'suites/tracing/flue/test.ts', + 'suites/vercel/keep-alive/test.ts', ]; // Bun does not publish `http.server.request.start`, so `@sentry/node` creates no `http.server` diff --git a/dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts b/dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts new file mode 100644 index 000000000000..15b47d11e9b6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/vercel/keep-alive/scenario.ts @@ -0,0 +1,124 @@ +// Models a Vercel Node.js function: the platform exposes a per-request context with `waitUntil`, +// stops accepting `waitUntil` work once the response has closed, and freezes the instance right after the +// registered work settles. Nothing else (unref'd timers, SIGTERM) runs after the freeze. +import { AsyncLocalStorage } from 'node:async_hooks'; +import * as http from 'node:http'; +import * as net from 'node:net'; +import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +interface RequestStore { + waitUntil: (task: Promise) => void; + closed: boolean; +} + +const als = new AsyncLocalStorage(); +const registered: Promise[] = []; +let late = 0; + +(globalThis as Record)[Symbol.for('@vercel/request-context')] = { + get: () => als.getStore(), +}; + +function loggingTransport(_options: BaseTransportOptions): Transport { + return { + send(envelope: Envelope): Promise { + // eslint-disable-next-line no-console + console.log(JSON.stringify(envelope)); + return Promise.resolve({ statusCode: 200 }); + }, + flush(): PromiseLike { + return Promise.resolve(true); + }, + }; +} + +// Cases: `default`, `no-tracing` (no `tracesSampleRate`), `head` (HEAD requests never get a server span), +// `slow` (the handler responds late) and `late-root` (the root span ends after the response closes). +const testCase = process.env.TEST_CASE || 'default'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: testCase === 'no-tracing' ? undefined : 1, + enableLogs: true, + traceLifecycle: process.env.TRACE_LIFECYCLE === 'stream' ? 'stream' : 'static', + transport: loggingTransport, + integrations: testCase === 'late-root' ? [Sentry.httpIntegration({ spans: false })] : [], +}); + +const server = http.createServer(async (_req, res) => { + if (testCase === 'slow') { + await new Promise(resolve => setTimeout(resolve, 300)); + } + + const respond = (): void => { + Sentry.logger.info('keep alive log'); + Sentry.metrics.count('keep_alive_metric', 1); + if (testCase === 'no-tracing') { + Sentry.captureException(new Error('keep alive error')); + } + // Without a server span (HEAD), a manual span would become a root span itself and mask the case. + if (testCase !== 'head') { + Sentry.startSpan({ name: 'keep-alive-child' }, () => undefined); + } + res.writeHead(200); + res.end('ok'); + }; + + if (testCase === 'late-root') { + // Some frameworks end the root span a moment after the response has closed. + Sentry.startSpanManual({ name: 'GET /', op: 'http.server' }, span => { + res.once('close', () => setTimeout(() => span.end(), 20)); + respond(); + }); + } else { + respond(); + } +}); + +// As on Vercel, `http.server.request.start` fires before the request context exists. The context is active +// for the handler and the response it writes. +const originalEmit = server.emit; +server.emit = function (this: http.Server, event: string, ...args: unknown[]): boolean { + if (event !== 'request') { + return originalEmit.call(this, event, ...args); + } + + const store: RequestStore = { + closed: false, + waitUntil: task => { + if (store.closed) { + late++; + } else { + registered.push(task); + } + }, + }; + // Registered before the original emit, so it runs before Sentry's own 'close' listeners. + (args[1] as http.ServerResponse).once('close', () => { + store.closed = true; + }); + return als.run(store, () => originalEmit.call(this, event, ...args)); +} as typeof server.emit; + +server.listen(0, async () => { + const { port } = server.address() as net.AddressInfo; + + // A raw socket keeps the request free of trace propagation headers, which would otherwise carry a + // sampling decision into the server span. + await new Promise(resolve => { + const socket = net.connect(port, 'localhost', () => { + socket.write( + `${testCase === 'head' ? 'HEAD' : 'GET'} / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n`, + ); + }); + socket.resume(); + socket.on('close', () => resolve()); + }); + + // The freeze: only what was registered in time keeps the instance alive. + await Promise.all(registered); + // eslint-disable-next-line no-console + console.log(`WAITUNTIL registered=${registered.length} late=${late}`); + process.exit(0); +}); diff --git a/dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts b/dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts new file mode 100644 index 000000000000..e104ddbfd051 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/vercel/keep-alive/test.ts @@ -0,0 +1,110 @@ +import { afterAll, describe, expect, test, vi } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe.each(['stream', 'static'] as const)('keeps the instance alive for telemetry (%s)', lifecycle => { + const logExpectation = { + log: { + items: [expect.objectContaining({ body: 'keep alive log' })], + }, + }; + + const rootSpanExpectation = + lifecycle === 'stream' + ? { + span: { + items: expect.arrayContaining([ + expect.objectContaining({ name: 'keep-alive-child', is_segment: false }), + expect.objectContaining({ is_segment: true }), + ]), + }, + } + : { + transaction: { + contexts: { trace: { op: 'http.server' } }, + spans: [expect.objectContaining({ description: 'keep-alive-child' })], + }, + }; + + const metricExpectation = { + trace_metric: { + items: [expect.objectContaining({ name: 'keep_alive_metric', value: 1 })], + }, + }; + + test('sends the root span, log and metric before the instance freezes', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle }) + .unordered() + .expect(rootSpanExpectation) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends the root span of a request that responds late', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'slow' }) + .unordered() + .expect(rootSpanExpectation) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends a root span that ends after the response closes', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'late-root' }) + .unordered() + .expect(rootSpanExpectation) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends errors and logs when tracing is disabled', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'no-tracing' }) + .unordered() + .expect({ + event: { + exception: { values: [expect.objectContaining({ value: 'keep alive error' })] }, + }, + }) + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); + + test('sends logs for requests that get no server span', async () => { + const runner = createRunner(__dirname, 'scenario.ts') + .withEnv({ VERCEL: '1', TRACE_LIFECYCLE: lifecycle, TEST_CASE: 'head' }) + .unordered() + .expect(logExpectation) + .expect(metricExpectation) + .start(); + + await runner.completed(); + + await vi.waitFor(() => expect(runner.getLogs().join('\n')).toContain('WAITUNTIL registered=1 late=0')); + }); +}); diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index b740fa3e6612..e4a9e64c6185 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -40,6 +40,7 @@ import { getSpotlightConfig } from '../utils/spotlight'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initOpenTelemetry } from './initOtel'; +import { setupVercelKeepAlive } from './vercel'; /** * Get the base default integrations shared by all Node SDK default-integration sets. @@ -189,13 +190,8 @@ function _init( updateScopeFromEnvVariables(); - // Ensure we flush events when vercel functions are ended - // See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal if (process.env.VERCEL) { - process.on('SIGTERM', async () => { - // We have 500ms for processing here, so we try to make sure to have enough time to send the events - await client.flush(200); - }); + setupVercelKeepAlive(client); } // Add Node SDK specific OpenTelemetry setup. `setupEventContextTrace` reads the active span from the diff --git a/packages/node/src/sdk/vercel.ts b/packages/node/src/sdk/vercel.ts new file mode 100644 index 000000000000..9a18c5e1c9a2 --- /dev/null +++ b/packages/node/src/sdk/vercel.ts @@ -0,0 +1,86 @@ +import type { Client, Span } from '@sentry/core'; +import { getActiveSpan, getRootSpan, GLOBAL_OBJ } from '@sentry/core'; +import type { HttpServerResponse } from '@sentry/core/server'; +import { _INTERNAL_safeUnref } from '@sentry/core/server'; +import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; + +// On Vercel, `http.server.request.start` fires without the request context, while this channel fires with it. +const HTTP_ON_SERVER_RESPONSE_FINISH = 'http.server.response.finish'; + +// Some frameworks end the request's root span shortly after the response closes. +const REQUEST_END_TIMEOUT_MS = 2000; + +interface VercelRequestContextGlobal { + get?(): { waitUntil?: (task: Promise) => void } | undefined; +} + +/** + * Keeps Vercel Node.js functions alive until the SDK has sent the telemetry of each request. + * + * Vercel can suspend a function as soon as the response is sent, so buffered events, spans, logs and metrics + * (and the timers that flush them) arrive late or never. When a response + * finishes, we register one `waitUntil` that waits for the response to close and the request's root span to + * end (2 seconds at most), and then flushes the client. + */ +export function setupVercelKeepAlive(client: Client): void { + // Ensure we flush events when vercel functions are ended + // See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal + process.on('SIGTERM', async () => { + // We have 500ms for processing here, so we try to make sure to have enough time to send the events + await client.flush(200); + }); + + subscribeDiagnosticsChannel(HTTP_ON_SERVER_RESPONSE_FINISH, message => { + const { response } = message as { response?: HttpServerResponse }; + const requestContextGlobal: VercelRequestContextGlobal | undefined = + // @ts-expect-error Vercel sets this global, so `GLOBAL_OBJ` does not type it + GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; + const requestContext = requestContextGlobal?.get?.(); + if (!response || !requestContext?.waitUntil) { + return; + } + + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan && getRootSpan(activeSpan); + + requestContext.waitUntil( + waitForRequestEnd(client, response, rootSpan, REQUEST_END_TIMEOUT_MS).then(() => client.flush(2000)), + ); + }); +} + +function waitForRequestEnd( + client: Client, + response: HttpServerResponse, + rootSpan: Span | undefined, + timeout: number, +): Promise { + return new Promise(resolve => { + let responseClosed = false; + // Ended and unsampled spans are not recording. + let rootSpanEnded = !rootSpan?.isRecording(); + + const done = (): void => { + clearTimeout(timer); + unsubscribe(); + resolve(); + }; + const doneIfRequestEnded = (): void => { + if (responseClosed && rootSpanEnded) { + done(); + } + }; + + const timer = _INTERNAL_safeUnref(setTimeout(done, timeout)); + const unsubscribe = client.on('spanEnd', span => { + if (span === rootSpan) { + rootSpanEnded = true; + doneIfRequestEnded(); + } + }); + response.once('close', () => { + responseClosed = true; + doneIfRequestEnded(); + }); + }); +} diff --git a/packages/node/test/sdk/vercel.test.ts b/packages/node/test/sdk/vercel.test.ts new file mode 100644 index 000000000000..211feef8bc60 --- /dev/null +++ b/packages/node/test/sdk/vercel.test.ts @@ -0,0 +1,170 @@ +import { EventEmitter } from 'node:events'; +import type { Client, Span } from '@sentry/core'; +import type * as SentryCore from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; +import type * as SentryServerUtils from '@sentry/server-utils'; +import { subscribeDiagnosticsChannel } from '@sentry/server-utils'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { setupVercelKeepAlive } from '../../src/sdk/vercel'; + +vi.mock('@sentry/core', async importOriginal => ({ + ...(await importOriginal()), + getActiveSpan: vi.fn(), +})); + +vi.mock('@sentry/server-utils', async importOriginal => ({ + ...(await importOriginal()), + subscribeDiagnosticsChannel: vi.fn(), +})); + +const REQUEST_CONTEXT = Symbol.for('@vercel/request-context'); + +function createClient() { + const spanEndCallbacks = new Set<(span: Span) => void>(); + return { + on: vi.fn((_hook: 'spanEnd', callback: (span: Span) => void) => { + spanEndCallbacks.add(callback); + return () => spanEndCallbacks.delete(callback); + }), + flush: vi.fn().mockResolvedValue(true), + endSpan: (span: Span) => spanEndCallbacks.forEach(callback => callback(span)), + }; +} + +function setActiveRootSpan(): Span { + const span = { isRecording: () => true } as unknown as Span; + vi.mocked(getActiveSpan).mockReturnValue(span); + return span; +} + +function finishResponse() { + const response = new EventEmitter(); + const [, onMessage] = vi.mocked(subscribeDiagnosticsChannel).mock.calls.at(-1)!; + onMessage({ request: new EventEmitter(), response }, 'http.server.response.finish'); + return response; +} + +describe('setupVercelKeepAlive', () => { + let waitUntil: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + waitUntil = vi.fn(); + (globalThis as any)[REQUEST_CONTEXT] = { get: () => ({ waitUntil }) }; + vi.spyOn(process, 'on').mockImplementation(() => process); + }); + + afterEach(() => { + (globalThis as any)[REQUEST_CONTEXT] = undefined; + vi.mocked(getActiveSpan).mockReturnValue(undefined); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it('registers exactly one waitUntil per finished response', () => { + setupVercelKeepAlive(createClient() as unknown as Client); + + // Vercel does not publish `http.server.request.start` for function requests. + expect(subscribeDiagnosticsChannel).toHaveBeenCalledWith('http.server.response.finish', expect.any(Function)); + + finishResponse(); + expect(waitUntil).toHaveBeenCalledTimes(1); + + finishResponse(); + expect(waitUntil).toHaveBeenCalledTimes(2); + }); + + it('keeps the task pending until the response closes, then flushes the client once', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + + const response = finishResponse(); + const task = waitUntil.mock.calls[0]![0] as Promise; + const settled = vi.fn(); + void task.then(settled); + + await vi.advanceTimersByTimeAsync(0); + expect(settled).not.toHaveBeenCalled(); + expect(client.flush).not.toHaveBeenCalled(); + + response.emit('close'); + await task; + + expect(client.flush).toHaveBeenCalledTimes(1); + expect(client.flush).toHaveBeenCalledWith(2000); + }); + + it('waits for the root span to end after the response closes, then flushes', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + const rootSpan = setActiveRootSpan(); + + const response = finishResponse(); + const task = waitUntil.mock.calls[0]![0] as Promise; + + response.emit('close'); + await vi.advanceTimersByTimeAsync(0); + expect(client.flush).not.toHaveBeenCalled(); + + client.endSpan(rootSpan); + await task; + + expect(client.flush).toHaveBeenCalledTimes(1); + }); + + it('flushes after 2 seconds when the root span does not end', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + setActiveRootSpan(); + + const response = finishResponse(); + response.emit('close'); + + await vi.advanceTimersByTimeAsync(1999); + expect(client.flush).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(1); + expect(client.flush).toHaveBeenCalledTimes(1); + }); + + it('flushes after 2 seconds when the response does not close', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + + finishResponse(); + + await vi.advanceTimersByTimeAsync(1999); + expect(client.flush).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(1); + expect(client.flush).toHaveBeenCalledTimes(1); + }); + + it('does nothing without a request context', () => { + (globalThis as any)[REQUEST_CONTEXT] = undefined; + setupVercelKeepAlive(createClient() as unknown as Client); + + finishResponse(); + + expect(waitUntil).not.toHaveBeenCalled(); + }); + + it('does nothing when the request context has no waitUntil', () => { + (globalThis as any)[REQUEST_CONTEXT] = { get: () => ({}) }; + setupVercelKeepAlive(createClient() as unknown as Client); + + expect(() => finishResponse()).not.toThrow(); + expect(waitUntil).not.toHaveBeenCalled(); + }); + + it('does not depend on a span', async () => { + const client = createClient(); + setupVercelKeepAlive(client as unknown as Client); + + const response = finishResponse(); + response.emit('close'); + await (waitUntil.mock.calls[0]![0] as Promise); + + expect(client.flush).toHaveBeenCalledTimes(1); + }); +}); From d93e210e440a281cf124f542aa0eed1f2a85e517 Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Tue, 6 Oct 2026 01:56:33 +0300 Subject: [PATCH 30/73] fix(server-utils): Instrument the Vercel AI experimental_decide channel event (#25068) Since `ai` 7.0.128, `experimental_evaluate` calls produce no `gen_ai.evaluate` span. The release renamed it to `experimental_decide`, and the tracing channel now publishes `type: "experimental_decide"`. Our subscriber only matched the old name. It now handles both, since 7.0.111 to 7.0.127 still publish `experimental_evaluate`. This broke the evaluate integration tests and the `node-eve` Jev e2e test on every PR, because the suite installs `ai@^7.0.111`. Co-authored-by: Claude Opus 5.5 --- .../vercel-ai/vercel-ai-dc-subscriber.ts | 7 ++- .../integrations/vercel-ai/evaluate.test.ts | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 2ea07e96251c..6923694b3a0c 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -217,7 +217,8 @@ export type ChannelEventType = | 'embed' | 'embedMany' | 'rerank' - | 'experimental_evaluate'; + | 'experimental_evaluate' + | 'experimental_decide'; /** * The context object the AI SDK passes through one tracing-channel call. It is the same object @@ -451,7 +452,9 @@ export function createSpanFromMessage( } case 'rerank': return startGenAiSpan('rerank', modelId, baseAttributes); + // `ai` 7.0.128 renamed `experimental_evaluate` to `experimental_decide`; older 7.x still publishes the old name. case 'experimental_evaluate': + case 'experimental_decide': return startGenAiSpan('evaluate', modelId, { ...baseAttributes, ...(recordInputs @@ -654,7 +657,7 @@ function getOutputMessages( result: Record, finishReason: string | undefined, ): string | undefined { - if (type === 'experimental_evaluate') { + if (type === 'experimental_evaluate' || type === 'experimental_decide') { return stringify([{ type: 'evaluation', answers: withProviderConfidence(result) }]); } // `languageModelCall` exposes the response as a `content` parts array; top-level results expose diff --git a/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts b/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts new file mode 100644 index 000000000000..0101b19cf69b --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/evaluate.test.ts @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { createSpanFromMessage, enrichSpanOnEnd } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +type Message = Parameters[0]; + +describe('Vercel AI evaluate spans', () => { + let endedSpans: Span[]; + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + }), + ); + setCurrentClient(client); + client.init(); + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + // `ai` 7.0.128 renamed the channel event from `experimental_evaluate` to `experimental_decide`. + it.each(['experimental_evaluate', 'experimental_decide'] as const)('creates an evaluate span for `%s`', type => { + const message = { + type, + event: { + provider: 'gateway', + modelId: 'typesafe-ai/jev', + state: 'I want a refund', + questions: { wantsRefund: { type: 'boolean', instructions: 'Is a refund requested?' } }, + recordInputs: true, + recordOutputs: true, + }, + result: { answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } }, + } as unknown as Message; + + const span = createSpanFromMessage(message, {} as Parameters[1])!; + expect(span).toBeDefined(); + enrichSpanOnEnd(span, message, {} as Parameters[2]); + span.end(); + + const json = spanToStaticSpanJSON(endedSpans[0]!); + expect(json.description).toBe('evaluate typesafe-ai/jev'); + expect(json.op).toBe('gen_ai.evaluate'); + expect(JSON.parse(json.data?.['gen_ai.output.messages'] as string)).toEqual([ + { type: 'evaluation', answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } }, + ]); + }); +}); From 6805115acce261272ce7eea21abb8c61daa0a14c Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 5 Oct 2026 16:33:26 -0700 Subject: [PATCH 31/73] feat(core): Warn on repeated `Sentry.init()` and unbind the client on `close()` (#24962) A repeated `Sentry.init()` call was mostly undefined behavior, and each SDK handled it in its own way. Most SDKs built a new client and replaced the old one without a warning. Nothing closed the old client, so its buffers, timers, and hooks stayed alive, and `setupOnce` kept the settings of the first call. The goal is one rule for all SDKs: the first `init()` wins, a later call returns the active client, and `close()` lets you start over. Most SDKs cannot switch to "first wins" before a major version, so this change adds the parts that are safe now: - `initAndBind`, Node's `_init`, and Vercel Edge's `init` print a warning (with or without `debug`) when a client is already bound. They still replace the client for now. Wrappers that expect a repeated call (Next.js server, Remix server, Nuxt server, Hono) keep their own guard and return early, so they do not warn. Cloudflare's `cacheClient: false` asks for a new client on each call, so it unbinds the old one first and does not warn. The Next.js client drops its own warning, which used a flag that never reset and so also fired after `close()`. Its config-file hint moves to the docs. - `Sentry.close()` unbinds the client after it closes it. Before, a guard based on `getClient()` treated the closed client as active, so `close(); init()` returned the closed client. Nuxt's server guard now also checks for a bound client, for the same reason. Cloudflare also caches its client for the isolate, and the cache handed the closed client back to every later `init()`, so the isolate sent nothing until it was recycled. Closing the cached client now clears the cache. - Next.js server and Remix server return the active client from a repeated call, not `undefined`. A caller could not tell "already initialized" from "failed". The Next.js and Nuxt server `init()` added their event processors to the global scope. Since `close()` now unbinds the client, a later `init()` would run the full setup again, and each cycle would add another copy of each processor to the global scope. So instead, add them to the client, so that they go away with a closed client, and a later `init()` uses its own options. Client processors run before all scope processors, so these now also run before any global scope processor that user code added before `init()`. They only drop events or fix stack frames, so the earlier position does not change which events are sent. `docs/repeated-init.md` records the rule, the current behavior of each SDK, and the plan for the next major. The warning text should point apps that share a page to the isolated client helper from #24883 once that helper has a final name. Fixes #24960 --------- Co-authored-by: Claude Opus 5.5 (1M context) --- .agents/skills/write-tests/SKILL.md | 28 +++- AGENTS.md | 3 + CHANGELOG.md | 2 + .../init-repeated/scenario-close-in-scope.ts | 23 +++ .../init-repeated/scenario-close.ts | 22 +++ .../init-repeated/scenario-repeated.ts | 16 ++ .../suites/public-api/init-repeated/test.ts | 38 +++++ docs/repeated-init.md | 77 +++++++++ packages/angular/test/sdk.test.ts | 6 +- packages/angular/test/tracing.test.ts | 8 +- packages/astro/test/client/sdk.test.ts | 1 + packages/browser/test/index.test.ts | 4 + .../browser/test/profiling/UIProfiler.test.ts | 8 +- .../test/profiling/integration.test.ts | 7 +- packages/browser/test/sdk.test.ts | 1 + packages/bun/test/init.test.ts | 3 +- .../test/integrations/bunHttpServer.test.ts | 1 + .../bun/test/integrations/bunserver.test.ts | 1 + packages/bun/test/sdk.test.ts | 2 + packages/cloudflare/src/baseSdk.ts | 12 +- packages/cloudflare/test/request.test.ts | 2 + packages/cloudflare/test/sdk.test.ts | 60 +++++++ packages/core/src/client.ts | 15 ++ packages/core/src/exports.ts | 13 +- packages/core/src/index.ts | 9 +- packages/core/src/sdk.ts | 34 +++- packages/core/test/lib/sdk.test.ts | 153 +++++++++++++++++- packages/deno/test/sdk.test.ts | 12 +- packages/feedback/test/core/mockSdk.ts | 3 + packages/hono/src/bun/sdk.ts | 4 +- packages/hono/src/deno/sdk.ts | 4 +- packages/hono/src/node/sdk.ts | 4 +- packages/hono/test/bun/middleware.test.ts | 16 +- packages/hono/test/deno/middleware.test.ts | 16 +- packages/hono/test/node/middleware.test.ts | 20 +-- packages/hono/test/node/sdk.test.ts | 8 +- packages/nextjs/src/client/index.ts | 18 +-- packages/nextjs/src/server/index.ts | 17 +- packages/nextjs/test/clientSdk.test.ts | 18 ++- packages/nextjs/test/edgeSdk.test.ts | 1 + packages/nextjs/test/serverSdk.test.ts | 36 ++++- packages/node/src/sdk/index.ts | 3 + packages/node/test/sdk/init.test.ts | 43 ++++- packages/nuxt/src/server/sdk.ts | 15 +- packages/nuxt/test/client/sdk.test.ts | 1 + packages/nuxt/test/server/sdk.test.ts | 53 ++++-- packages/react/test/normalize.test.ts | 3 +- packages/react/test/sdk.test.ts | 6 +- packages/remix/src/server/instrumentServer.ts | 5 + packages/remix/src/server/sdk.ts | 9 +- packages/remix/test/index.client.test.ts | 3 +- packages/remix/test/index.server.test.ts | 18 +++ .../test/server/instrumentServer.test.ts | 23 ++- .../replay-internal/test/mocks/mockSdk.ts | 3 + .../mysql2/mysql2-dc-subscriber.test.ts | 2 + .../test/integrations/prisma.test.ts | 3 +- packages/solidstart/test/client/sdk.test.ts | 5 + packages/solidstart/test/server/sdk.test.ts | 1 + packages/svelte/test/sdk.test.ts | 1 + packages/sveltekit/test/client/fetch.test.ts | 4 +- .../test/client/sdk.test.ts | 1 + .../test/server/sdk.test.ts | 1 + packages/vercel-edge/src/sdk.ts | 3 + packages/vercel-edge/test/sdk.test.ts | 49 +++++- .../test/integration/VueIntegration.test.ts | 1 + packages/vue/test/integration/init.test.ts | 1 + .../integration/mixinRegistration.test.ts | 1 + .../vue/test/integration/normalize.test.ts | 1 + 68 files changed, 859 insertions(+), 126 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts create mode 100644 docs/repeated-init.md diff --git a/.agents/skills/write-tests/SKILL.md b/.agents/skills/write-tests/SKILL.md index af8f0408e8b9..ba876f350f1d 100644 --- a/.agents/skills/write-tests/SKILL.md +++ b/.agents/skills/write-tests/SKILL.md @@ -307,16 +307,31 @@ it.each([ ### Test isolation Tests must never depend on execution order or share mutable state. For this codebase, many tests -need to reset global Sentry state: +need to reset global Sentry state. Use whatever reset the existing tests in your package use (many +packages have a `resetSdk()` or `cleanupOtel()` helper). Otherwise, pick one: ```typescript -beforeEach(() => { - clearGlobalScope(); - getCurrentScope().clear(); - getIsolationScope().clear(); -}); +// Unbind the client. Cheap, and enough for most tests that call `init()`. +getCurrentScope().setClient(undefined); + +// Reset the global, isolation, and current scopes, and the client bound to them. +// `setupOnce` still does not run again, because installed integrations are tracked +// outside the carrier. +getMainCarrier().__SENTRY__ = undefined; + +// Close and unbind the client. Also flushes, so it is slower. +await Sentry.close(); ``` +**Reset the client in every test that calls `init()`.** A second `init()` while a client is bound +prints a "`Sentry.init()` was called more than once" warning. After the next major version, it will +return the bound client instead of a new one, so a test without a reset gets the previous test's +client and options. See `docs/repeated-init.md`. + +To assert on that warning (or any `consoleSandbox` output), stub `originalConsoleMethods.warn` from +`@sentry/core`. `consoleSandbox` calls the stored original method, so a spy on `console.warn` misses +it once the console integration is set up. + ### Grouping 1-2 levels of `describe` is usually enough. Deeper nesting makes tests harder to find and read. @@ -526,6 +541,7 @@ Before you're done, verify each test against these criteria: - [ ] Description reads as a behavior specification (no "should", no "works correctly") - [ ] No dependency on other tests' execution or state - [ ] Mocks and spies are restored (via `beforeEach`) +- [ ] Tests that call `init()` reset the bound client between tests - [ ] Edge cases covered: empty inputs, boundaries, error paths, null/undefined - [ ] Realistic test data (not `"foo"`, `"test"`, `123`) - [ ] No try/catch for error testing — `toThrow` / `rejects.toThrow` only diff --git a/AGENTS.md b/AGENTS.md index 7947db750390..c1ac68c704ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,9 @@ Uses **Git Flow** (see `docs/gitflow.md`). Runtime packages (`node`, `cloudflare`, ...) re-export from `@sentry/server-utils` rather than defining their own. +- `Sentry.init()` follows one rule for repeated calls: the first call wins. See + `docs/repeated-init.md` before you add or change an `init()`. + ## Linting & Formatting - This project uses **Oxlint** and **Oxfmt** — NOT ESLint or Prettier diff --git a/CHANGELOG.md b/CHANGELOG.md index 9452bacdf056..96ff401eb033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,8 @@ Work in this release was contributed by @Shubham-Padkonde and @tobias-schnabel. Thank you for your contributions! +- **feat(core)**: `Sentry.init()` now warns when it runs while a client is still active. For now, the new client still replaces the active client, but the active client is not closed, so state from both can mix. Call `Sentry.init()` once, or call `await Sentry.close()` before you call it again. `Sentry.close()` now unbinds the client it closes, so after `close()`, `getClient()` returns `undefined`, `isInitialized()` returns `false`, and a later `init()` sets up a new client. In `@sentry/cloudflare`, later requests no longer reuse the isolate's cached client once it is closed or closing. On the server, `@sentry/nextjs` and `@sentry/remix` now return the active client from a repeated `init()` call, not `undefined`. + ## 11.2.0 ### Important Changes diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts new file mode 100644 index 000000000000..db5aaf531c0c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close-in-scope.ts @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +async function run(): Promise { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + }); + + // The root scope still holds the closed client after this. + await Sentry.withIsolationScope(() => Sentry.close()); + + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '2.0', + transport: loggingTransport, + }); + + Sentry.captureMessage('after close in a child scope and init'); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts new file mode 100644 index 000000000000..7edc88abd2ba --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-close.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +async function run(): Promise { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + }); + + await Sentry.close(); + + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '2.0', + transport: loggingTransport, + }); + + Sentry.captureMessage('after close and init'); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts new file mode 100644 index 000000000000..1c6b4c4074a6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/scenario-repeated.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '2.0', + transport: loggingTransport, +}); + +Sentry.captureMessage('after repeated init'); diff --git a/dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts b/dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts new file mode 100644 index 000000000000..df98963ce645 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/init-repeated/test.ts @@ -0,0 +1,38 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +const REPEATED_INIT_WARNING = '`Sentry.init()` was called more than once'; + +test('warns on a repeated init and sends events with the new client', async () => { + const runner = createRunner(__dirname, 'scenario-repeated.ts') + .expect({ event: { message: 'after repeated init', release: '2.0' } }) + .start(); + + await runner.completed(); + + expect(runner.getLogs().join('\n')).toContain(REPEATED_INIT_WARNING); +}); + +test('does not warn on init after close and sends events with the new client', async () => { + const runner = createRunner(__dirname, 'scenario-close.ts') + .expect({ event: { message: 'after close and init', release: '2.0' } }) + .start(); + + await runner.completed(); + + expect(runner.getLogs().join('\n')).not.toContain(REPEATED_INIT_WARNING); +}); + +test('does not warn on init after close in a child scope', async () => { + const runner = createRunner(__dirname, 'scenario-close-in-scope.ts') + .expect({ event: { message: 'after close in a child scope and init', release: '2.0' } }) + .start(); + + await runner.completed(); + + expect(runner.getLogs().join('\n')).not.toContain(REPEATED_INIT_WARNING); +}); diff --git a/docs/repeated-init.md b/docs/repeated-init.md new file mode 100644 index 000000000000..aa12aaf5bd23 --- /dev/null +++ b/docs/repeated-init.md @@ -0,0 +1,77 @@ +# Repeated `Sentry.init()` Calls + +This document records how `Sentry.init()` should act when an app calls it +more than once. Use it when you add or change an `init()` in any SDK. + +## The rule + +> One active client per `init()` target. The first `init()` wins. A later +> `init()` changes nothing, returns the active client, and warns. To +> reconfigure, call `close()` first. + +"Active" means a client is bound to the current scope and is not closed. +`Sentry.close()` closes the client and unbinds it, so a later `init()` sets +up a new client. Other scopes can still hold the closed client (for example, +when `close()` runs inside a request, or when code calls `client.close()` +directly), so a check for "already initialized" must not use `getClient()` +alone. + +A repeated `init()` is not supported. Until the next major version, most +SDKs still replace the client (see below). Do not depend on that. + +## Why + +When `init()` replaces a client, nothing closes the old one: + +- Buffered logs, metrics, spans, client reports, and flush timers stay on + the old client. +- `setupOnce` runs only for the first client, because the list of + installed integrations is global. The result mixes settings from both + calls. +- `initialScope` merges into the current scope on each call. + +"First wins" never gives a user half of one config and half of another. + +## Current behavior + +| Entry point | Repeated call | +| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `initAndBind` (browser and its wrappers, Deno), Node `_init`, Vercel Edge | Warns, then replaces the client. Returns the new client. | +| Next.js server, Remix server, Hono Node | Keeps the first client and returns it. Logs in debug mode only. | +| Hono Bun and Deno | Keeps the first client and returns it. Warns with its own text. | +| Nuxt server | Keeps the first client and returns it. Logs that a `--import` preload is no longer needed. | +| Cloudflare (default) | Keeps the first client of the isolate and returns it, unless that client is closed or closing. `cacheClient: false` makes a new client on each call, with no warning. | +| Next.js edge | Warns, then replaces the client. Returns `void`. | + +The shared warning lives in `warnIfClientIsActive()` in +`packages/core/src/sdk.ts`. Core exports it as +`_INTERNAL_warnIfClientIsActive` for SDKs that build their client without +`initAndBind`. + +A wrapper that expects a repeated call, such as a server bundle and a +`--import` preload that both run the config, keeps its own guard and +returns early. The shared warning then does not show. Guards use +`getActiveClient()` (exported as `_INTERNAL_getActiveClient`), which +returns the bound client only if it is not closed. + +## TODO(v12): Plan for the next major version + +1. Move the "first wins" guard into `initAndBind`, Node's `_init`, and + Vercel Edge's `init`. Remove the guards in each wrapper. +2. Switch browser to "first wins". For two apps on one page, point users + to separate clients that are not bound with `init()` (see #24883). +3. Make Next.js edge return the client. + +## Tests + +A test that calls `init()` again without a reset now prints the warning. +Reset between tests with one of these: + +- `getCurrentScope().setClient(undefined)`, or a helper that clears the + carrier (`getMainCarrier().__SENTRY__ = undefined`). +- `await Sentry.close()`. This also flushes, so it is slower. + +The warning goes through `consoleSandbox`, which calls the method stored +in `originalConsoleMethods`. To assert on it, replace +`originalConsoleMethods.warn` with a mock. A spy on `console.warn` misses +it once the console integration is set up. diff --git a/packages/angular/test/sdk.test.ts b/packages/angular/test/sdk.test.ts index 25beb721a448..4d6821e5642c 100644 --- a/packages/angular/test/sdk.test.ts +++ b/packages/angular/test/sdk.test.ts @@ -1,8 +1,12 @@ import * as SentryBrowser from '@sentry/browser'; -import { vi } from 'vitest'; +import { afterEach, vi } from 'vitest'; import { getDefaultIntegrations, init } from '../src/sdk'; describe('init', () => { + afterEach(() => { + SentryBrowser.getCurrentScope().setClient(undefined); + }); + it('sets the Angular version (if available) in the global scope', () => { const setContextSpy = vi.spyOn(SentryBrowser, 'setContext'); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 3139125c36db..769882727288 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,7 +1,7 @@ import { ElementRef } from '@angular/core'; import type { ActivatedRouteSnapshot } from '@angular/router'; -import { getMainCarrier, SentrySpan, spanToJSON, startSpan } from '@sentry/core'; -import { describe, it } from 'vitest'; +import { getCurrentScope, getMainCarrier, SentrySpan, spanToJSON, startSpan } from '@sentry/core'; +import { beforeEach, describe, it } from 'vitest'; import { browserTracingIntegration, init, TraceClass, TraceDirective } from '../src/index'; import { _updateSpanAttributesForParametrizedUrl, getParameterizedRouteFromSnapshot } from '../src/tracing'; import { @@ -74,6 +74,10 @@ describe('Angular Tracing', () => { }); describe('TraceService', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + it('change the span name to route name if the the source is `url`', async () => { init({ integrations: [browserTracingIntegration()] }); diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 041034f9164e..25d81333f194 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -98,6 +98,7 @@ describe('Sentry client SDK', () => { expect.objectContaining({ routeProvider: expect.objectContaining({ resolveRoute: expect.any(Function) }) }), ); + getMainCarrier().__SENTRY__ = undefined; const routeProvider = { resolveRoute: () => '/custom', resolveCurrentRoute: () => '/custom' }; init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', routeProvider }); expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider })); diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index e9836c90308b..c1c55dbca8ca 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -391,6 +391,10 @@ describe('SentryBrowser', () => { }); describe('SentryBrowser initialization', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + it('should use window.SENTRY_RELEASE to set release on initialization if available', () => { global.SENTRY_RELEASE = { id: 'foobar' }; init({ dsn }); diff --git a/packages/browser/test/profiling/UIProfiler.test.ts b/packages/browser/test/profiling/UIProfiler.test.ts index 38ac888f4d1e..a977693e4189 100644 --- a/packages/browser/test/profiling/UIProfiler.test.ts +++ b/packages/browser/test/profiling/UIProfiler.test.ts @@ -21,8 +21,7 @@ function getBaseOptionsForTraceLifecycle(sendMock: Mock, enableTracing = tr describe('Browser Profiling v2 trace lifecycle', () => { afterEach(async () => { - const client = Sentry.getClient(); - await client?.close(); + await Sentry.close(); // reset profiler constructor (window as any).Profiler = undefined; vi.restoreAllMocks(); @@ -568,7 +567,7 @@ describe('Browser Profiling v2 trace lifecycle', () => { } // End Session 1 - await client?.close(); + await Sentry.close(); // Session 2 (new init simulates new user session) const send2 = vi.fn().mockResolvedValue(undefined); @@ -737,8 +736,7 @@ function getBaseOptionsForManualLifecycle(sendMock: Mock, enableTracing = t describe('Browser Profiling v2 manual lifecycle', () => { afterEach(async () => { - const client = Sentry.getClient(); - await client?.close(); + await Sentry.close(); // reset profiler constructor (window as any).Profiler = undefined; vi.restoreAllMocks(); diff --git a/packages/browser/test/profiling/integration.test.ts b/packages/browser/test/profiling/integration.test.ts index 0bd7e96d8b1d..88e68f5f03bb 100644 --- a/packages/browser/test/profiling/integration.test.ts +++ b/packages/browser/test/profiling/integration.test.ts @@ -7,14 +7,19 @@ import { getClient, spanToStaticSpanJSON, getActiveSpan, + getCurrentScope, browserTracingIntegration, browserProfilingIntegration, } from '../../src/index'; import { debug } from '@sentry/core'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { BrowserClient } from '../../src/index'; describe('BrowserProfilingIntegration', () => { + beforeEach(() => { + getCurrentScope().setClient(undefined); + }); + it('profiles an already active pageload span in trace lifecycle mode', async () => { const stopProfile = vi.fn().mockResolvedValue({ frames: [{ name: 'pageload_fn', line: 1, column: 1 }], diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 7b983af3635e..79c029c0eb32 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -34,6 +34,7 @@ export class MockIntegration implements Integration { describe('init', () => { afterEach(() => { + SentryCore.getCurrentScope().setClient(undefined); vi.restoreAllMocks(); }); diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts index 2fced80fd67a..8a2929dd7dcf 100644 --- a/packages/bun/test/init.test.ts +++ b/packages/bun/test/init.test.ts @@ -1,4 +1,4 @@ -import { type Integration } from '@sentry/core'; +import { getCurrentScope, type Integration } from '@sentry/core'; import * as sentryServerUtils from '@sentry/server-utils'; import type { Mock } from 'bun:test'; import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test'; @@ -25,6 +25,7 @@ describe('init()', () => { let mockGetTracingIntegrations: Mock<() => Integration[]>; beforeEach(() => { + getCurrentScope().setClient(undefined); mockGetTracingIntegrations = spyOn(sentryServerUtils, 'getTracingIntegrations'); }); diff --git a/packages/bun/test/integrations/bunHttpServer.test.ts b/packages/bun/test/integrations/bunHttpServer.test.ts index 62b008bafe4a..9c679e69749f 100644 --- a/packages/bun/test/integrations/bunHttpServer.test.ts +++ b/packages/bun/test/integrations/bunHttpServer.test.ts @@ -22,6 +22,7 @@ function currentTraceId(): string | undefined { describe('Bun HTTP Server Integration', () => { beforeAll(() => { + getCurrentScope().setClient(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 391b3c981c59..a0cb31bed280 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -14,6 +14,7 @@ describe('Bun Serve Integration', () => { }); const setupClient = (options?: BunOptions): void => { + SentryCore.getCurrentScope().setClient(undefined); init({ dsn: 'https://username@domain/123', defaultIntegrations: false, diff --git a/packages/bun/test/sdk.test.ts b/packages/bun/test/sdk.test.ts index 0f414bc7495e..56814809d1ae 100644 --- a/packages/bun/test/sdk.test.ts +++ b/packages/bun/test/sdk.test.ts @@ -1,4 +1,5 @@ import type { BaseTransportOptions, Envelope, Event, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { describe, expect, test } from 'bun:test'; import type { NodeClient } from '../src/index'; import { init } from '../src/index'; @@ -26,6 +27,7 @@ describe('Bun SDK', () => { test('SDK works as expected', async () => { let client: NodeClient | undefined; + getCurrentScope().setClient(undefined); expect(() => { client = init(initOptions); }).not.toThrow(); diff --git a/packages/cloudflare/src/baseSdk.ts b/packages/cloudflare/src/baseSdk.ts index 94bdb6c17be3..0375e7f94601 100644 --- a/packages/cloudflare/src/baseSdk.ts +++ b/packages/cloudflare/src/baseSdk.ts @@ -1,5 +1,5 @@ import type { Integration } from '@sentry/core'; -import { debug, getCurrentScope, setCurrentClient } from '@sentry/core'; +import { _INTERNAL_isClientClosed, debug, getCurrentScope, setCurrentClient } from '@sentry/core'; import { consoleIntegration, conversationIdIntegration, @@ -94,7 +94,9 @@ export function initWithDefaultIntegrations( const cached = getCachedClient(); const cacheEnabled = options.cacheClient !== false; - if (cacheEnabled && cached) { + // A closed client drops all data. `close()` marks the client before it + // flushes, so this also skips a client that is still closing. + if (cacheEnabled && cached && !_INTERNAL_isClientClosed(cached)) { if (DEBUG_BUILD && cached.getOptions().dsn !== options.dsn) { debug.warn( '[Sentry] init() was called with a different DSN than the cached client of this isolate; the cached client keeps its DSN. Pass `cacheClient: false` for per-invocation options.', @@ -135,6 +137,12 @@ export function initWithDefaultIntegrations( } /*! rollup-include-development-only-end */ + if (!cacheEnabled) { + // `cacheClient: false` asks for a new client on each call, so replacing + // the bound client is expected here and must not warn. + getCurrentScope().setClient(undefined); + } + const client = initAndBind(CloudflareClient, clientOptions) as CloudflareClient; if (cacheEnabled && client) { diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 249da91d8cd1..ae30155c3c6a 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -1174,6 +1174,7 @@ describe('cached client (cacheClient)', () => { beforeEach(() => { vi.clearAllMocks(); _clearGlobalClientCache(); + SentryCore.getCurrentScope().setClient(undefined); }); // `init()` resolves defaults into the options object it is given, so each call @@ -1221,6 +1222,7 @@ describe('cached client (cacheClient)', () => { test('clears cache with _clearGlobalClientCache', async () => { const client1 = init(makeOptions()); _clearGlobalClientCache(); + SentryCore.getCurrentScope().setClient(undefined); const client2 = init(makeOptions()); expect(client2).not.toBe(client1); }); diff --git a/packages/cloudflare/test/sdk.test.ts b/packages/cloudflare/test/sdk.test.ts index 40dbca79fb84..d7820ecc343e 100644 --- a/packages/cloudflare/test/sdk.test.ts +++ b/packages/cloudflare/test/sdk.test.ts @@ -151,6 +151,45 @@ describe('cacheClient', () => { expect(getClient()).toBe(cached); }); + test('sets up a new cached client after close()', async () => { + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + } as const; + + const first = init({ ...options }); + await SentryCore.close(); + const second = init({ ...options }); + + expect(second).not.toBe(first); + expect(second?.getOptions().enabled).not.toBe(false); + expect(getClient()).toBe(second); + expect(init({ ...options })).toBe(second); + }); + + test('sets up a new cached client while the cached client is closing', async () => { + const options = { + dsn: 'https://public@dsn.ingest.sentry.io/1337', + } as const; + + const first = init({ ...options }); + const closing = first!.close(); + const second = init({ ...options }); + await closing; + + expect(second).not.toBe(first); + expect(second?.getOptions().enabled).not.toBe(false); + expect(init({ ...options })).toBe(second); + }); + + test('keeps the cached client when a client that is not cached closes', async () => { + const cached = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + const uncached = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); + + await uncached?.close(); + + expect(init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' })).toBe(cached); + }); + test('caches a client without a DSN and reuses it', () => { // A disabled (DSN-less) client is still created once per isolate, not per invocation. const first = init({}); @@ -189,6 +228,26 @@ describe('cacheClient', () => { expect(flushSpy).toHaveBeenCalled(); }); + test('does not warn about a repeated init when cacheClient is disabled', () => { + const originalWarn = SentryCore.originalConsoleMethods.warn; + const warn = vi.fn(); + SentryCore.originalConsoleMethods.warn = warn; + + try { + const first = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); + const second = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); + + expect(second).not.toBe(first); + expect(warn).not.toHaveBeenCalled(); + } finally { + if (originalWarn) { + SentryCore.originalConsoleMethods.warn = originalWarn; + } else { + delete SentryCore.originalConsoleMethods.warn; + } + } + }); + test('does not flush eagerly when cacheClient is disabled', async () => { const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', cacheClient: false }); @@ -343,6 +402,7 @@ describe('getDefaultIntegrations', () => { }); test('installs an integration registered after init via the module-injected event', async () => { + resetSdk(); const { mysqlIntegration } = await import('@sentry/server-utils'); const client = init({}); expect(client?.getIntegrationByName('Mysql')).toBeUndefined(); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 12d02b348bb2..9f77aceb9ed5 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -67,6 +67,20 @@ const DO_NOT_SEND_EVENT_SYMBOL = Symbol.for('SentryDoNotSendEventError'); // Default interval for flushing logs and metrics (5 seconds) const DEFAULT_FLUSH_INTERVAL = 5000; +// Clones of a scope keep their own reference to its client, so unbinding a +// closed client from one scope does not reach the others. The mark lives on +// the client under a `Symbol.for` key, so every copy of the SDK can read it. +const CLOSED_CLIENT_SYMBOL = Symbol.for('SentryClosedClient'); + +/** + * Whether `close()` was called on the given client. + * + * @hidden + */ +export function isClientClosed(client: Client): boolean { + return CLOSED_CLIENT_SYMBOL in client; +} + interface InternalError { message: string; [INTERNAL_ERROR_SYMBOL]: true; @@ -488,6 +502,7 @@ export abstract class Client { */ // @ts-expect-error - PromiseLike is a subset of Promise public async close(timeout?: number): PromiseLike { + Object.defineProperty(this, CLOSED_CLIENT_SYMBOL, { value: true }); const result = await this.flush(timeout); this.getOptions().enabled = false; this.emit('close'); diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 12623aa224d9..19e9509ea3a0 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -195,7 +195,8 @@ export async function flush(timeout?: number): Promise { } /** - * Call `close()` on the current client, if there is one. See {@link Client.close}. + * Call `close()` on the current client, if there is one, and unbind it from the current scope. + * See {@link Client.close}. * * @param timeout Maximum time in ms the client should wait to flush its event queue before shutting down. Omitting this * parameter will cause the client to wait until all events are sent before disabling itself. @@ -203,9 +204,15 @@ export async function flush(timeout?: number): Promise { * doesn't (or if there's no client defined). */ export async function close(timeout?: number): Promise { - const client = getClient(); + const scope = getCurrentScope(); + const client = scope.getClient(); if (client) { - return client.close(timeout); + const result = await client.close(timeout); + // Unbind the client, so a later `init()` sets up a new one. + if (scope.getClient() === client) { + scope.setClient(undefined); + } + return result; } DEBUG_BUILD && debug.warn('Cannot flush events and disable SDK. No client defined.'); return Promise.resolve(false); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 20b52be30603..d3bfe71b64a4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -58,8 +58,13 @@ export { Scope } from './scope'; export type { CaptureContext, ScopeContext, ScopeData } from './scope'; export { notifyEventProcessors } from './eventProcessors'; export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint, SENTRY_API_VERSION } from './api'; -export { Client } from './client'; -export { initAndBind, setCurrentClient } from './sdk'; +export { Client, isClientClosed as _INTERNAL_isClientClosed } from './client'; +export { + getActiveClient as _INTERNAL_getActiveClient, + initAndBind, + setCurrentClient, + warnIfClientIsActive as _INTERNAL_warnIfClientIsActive, +} from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from './transports/multiplexed'; diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index e7f20432b91a..98cd8f894182 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,5 +1,6 @@ import type { Client } from './client'; -import { getCurrentScope } from './currentScopes'; +import { isClientClosed } from './client'; +import { getClient, getCurrentScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { ClientOptions } from './types/options'; import { consoleSandbox, debug } from './utils/debug-logger'; @@ -29,6 +30,8 @@ export function initAndBind( }); } } + warnIfClientIsActive(); + const scope = getCurrentScope(); scope.update(options.initialScope); @@ -38,6 +41,35 @@ export function initAndBind( return client; } +/** + * Returns the client bound to the current scope, unless it is closed. + * + * @hidden + */ +export function getActiveClient(): C | undefined { + const client = getClient(); + return client && !isClientClosed(client) ? client : undefined; +} + +/** + * Warns when `init()` runs while a client is still bound. The new client + * replaces it, but the old client stays alive, so the two can mix state. + * + * @hidden + */ +export function warnIfClientIsActive(): void { + if (getActiveClient()) { + consoleSandbox(() => { + // TODO(#24883): Point apps that share a page with another app to the + // isolated client helper, once that PR settles its name. + // eslint-disable-next-line no-console + console.warn( + '[Sentry] `Sentry.init()` was called more than once. The new client replaces the active client, but the active client is not closed, so state from both can mix. Call `Sentry.init()` only once, or call `await Sentry.close()` before you call it again.', + ); + }); + } +} + /** * Make the given client the current client. */ diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 8ca2f6cda39d..8388c1d86434 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,9 +1,11 @@ -import { beforeEach, describe, expect, it, type Mock, test, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, type Mock, test, vi } from 'vitest'; import type { Client } from '../../src/client'; -import { getCurrentScope } from '../../src/currentScopes'; +import { getClient, getCurrentScope, withScope } from '../../src/currentScopes'; +import { close } from '../../src/exports'; import { captureCheckIn } from '../../src/monitor'; import { installedIntegrations } from '../../src/integration'; -import { initAndBind, setCurrentClient } from '../../src/sdk'; +import { originalConsoleMethods } from '../../src/utils/debug-logger'; +import { getActiveClient, initAndBind, setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types/integration'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; @@ -90,6 +92,151 @@ describe('SDK', () => { const client = initAndBind(TestClient, options); expect(client).not.toBeUndefined(); }); + + describe('when called again', () => { + // `consoleSandbox` calls the method stored in `originalConsoleMethods`, so a + // spy on `console.warn` misses the warning once the console is instrumented. + const originalWarn = originalConsoleMethods.warn; + let warnSpy: Mock; + + beforeEach(() => { + warnSpy = vi.fn(); + originalConsoleMethods.warn = warnSpy; + }); + + afterEach(() => { + if (originalWarn) { + originalConsoleMethods.warn = originalWarn; + } else { + delete originalConsoleMethods.warn; + } + }); + + test('does not warn on the first call', () => { + initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + test('warns and replaces the active client', () => { + const first = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`Sentry.init()` was called more than once')); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + + test('does not warn after close() in a child scope', async () => { + initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await withScope(() => close()); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + + test('does not warn after client.close()', async () => { + const first = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await first.close(); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + + test('does not warn after close()', async () => { + const first = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await close(); + const second = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + }); + }); +}); + +describe('getActiveClient', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + test('returns the bound client', () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + + expect(getActiveClient()).toBe(client); + }); + + test('returns undefined when no client is bound', () => { + expect(getActiveClient()).toBeUndefined(); + }); + + test('returns undefined for a closed client that is still bound', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await client.close(); + + expect(getClient()).toBe(client); + expect(getActiveClient()).toBeUndefined(); + }); + + test('returns undefined for a client that is still closing', () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + void client.close(); + + expect(getActiveClient()).toBeUndefined(); + }); + + test('returns undefined for a client that was closed twice', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await client.close(); + await client.close(); + + expect(getActiveClient()).toBeUndefined(); + }); + + test('sees a client closed through another copy of the SDK as closed', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await client.close(); + + vi.resetModules(); + const otherCopy = await import('../../src/client'); + + expect(otherCopy.isClientClosed(client)).toBe(true); + }); + + test('returns a client that was created with enabled: false', () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enabled: false })); + + expect(getActiveClient()).toBe(client); + }); +}); + +describe('close', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + test('unbinds the closed client', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + await close(); + + expect(client.getOptions().enabled).toBe(false); + expect(getClient()).toBeUndefined(); + }); + + test('does not unbind a client that was bound while closing', async () => { + const client = initAndBind(TestClient, getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + const other = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); + vi.spyOn(client, 'close').mockImplementation(async () => { + setCurrentClient(other); + return true; + }); + + await close(); + + expect(getClient()).toBe(other); }); }); diff --git a/packages/deno/test/sdk.test.ts b/packages/deno/test/sdk.test.ts index ad7152cb21ba..fe85e2ff9f45 100644 --- a/packages/deno/test/sdk.test.ts +++ b/packages/deno/test/sdk.test.ts @@ -1,9 +1,19 @@ import { assertNotEquals } from 'https://deno.land/std@0.202.0/assert/assert_not_equals.ts'; import { assertArrayIncludes } from 'https://deno.land/std@0.212.0/assert/assert_array_includes.ts'; -import { getDefaultIntegrations, init, spanStreamingIntegration } from '../build/esm/index.js'; +import { + getCurrentScope, + getDefaultIntegrations, + init as initSdk, + spanStreamingIntegration, +} from '../build/esm/index.js'; import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts'; import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts'; +function init(options) { + getCurrentScope().setClient(undefined); + return initSdk(options); +} + Deno.test('init() should return client', () => { assertNotEquals(init({}), undefined); }); diff --git a/packages/feedback/test/core/mockSdk.ts b/packages/feedback/test/core/mockSdk.ts index 5e7a65b38212..a15667791f4c 100644 --- a/packages/feedback/test/core/mockSdk.ts +++ b/packages/feedback/test/core/mockSdk.ts @@ -1,4 +1,5 @@ import type { Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { vi } from 'vitest'; import type { TestClientOptions } from '../../src/core/TestClient'; import { getDefaultClientOptions, init } from '../../src/core/TestClient'; @@ -43,6 +44,8 @@ class MockTransport implements Transport { * */ export async function mockSdk({ sentryOptions }: MockSdkParams = {}): Promise { + getCurrentScope().setClient(undefined); + init({ ...getDefaultClientOptions(), dsn: 'https://dsn@ingest.f00.f00/1', diff --git a/packages/hono/src/bun/sdk.ts b/packages/hono/src/bun/sdk.ts index 26f9c987348b..6b6b2170f085 100644 --- a/packages/hono/src/bun/sdk.ts +++ b/packages/hono/src/bun/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, consoleSandbox, getClient } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, consoleSandbox } from '@sentry/core'; import { init as initBun } from '@sentry/bun'; import type { HonoBunOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -13,7 +13,7 @@ import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../shared/lowQualityTransactio * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration. */ export function init(options: HonoBunOptions): Client | undefined { - const existingClient = getClient(); + const existingClient = _INTERNAL_getActiveClient(); if (existingClient) { consoleSandbox(() => { // eslint-disable-next-line no-console diff --git a/packages/hono/src/deno/sdk.ts b/packages/hono/src/deno/sdk.ts index 1c40bdb3bdbf..144a4391ee1e 100644 --- a/packages/hono/src/deno/sdk.ts +++ b/packages/hono/src/deno/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, consoleSandbox, getClient } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, consoleSandbox } from '@sentry/core'; import { init as initDeno } from '@sentry/deno'; import type { HonoDenoOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -13,7 +13,7 @@ import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../shared/lowQualityTransactio * When manually calling `init`, add the `honoIntegration` to the `integrations` array to set up the Hono integration. */ export function init(options: HonoDenoOptions): Client | undefined { - const existingClient = getClient(); + const existingClient = _INTERNAL_getActiveClient(); if (existingClient) { consoleSandbox(() => { // eslint-disable-next-line no-console diff --git a/packages/hono/src/node/sdk.ts b/packages/hono/src/node/sdk.ts index 61c8afe32f35..6203900e0db6 100644 --- a/packages/hono/src/node/sdk.ts +++ b/packages/hono/src/node/sdk.ts @@ -1,5 +1,5 @@ import type { Client } from '@sentry/core'; -import { applySdkMetadata, debug, getClient } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, debug } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import type { HonoNodeOptions } from './middleware'; import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations'; @@ -11,7 +11,7 @@ import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../shared/lowQualityTransactio * This function should be called in an `instrument.ts` file loaded via `--import` to set up Sentry globally for the application. */ export function init(options: HonoNodeOptions): Client | undefined { - const existingClient = getClient(); + const existingClient = _INTERNAL_getActiveClient(); if (existingClient) { existingClient.getOptions().debug && debug.log('Sentry is already initialized, skipping re-initialization.'); return existingClient; diff --git a/packages/hono/test/bun/middleware.test.ts b/packages/hono/test/bun/middleware.test.ts index f2ef49fa19a8..a8ce3baac746 100644 --- a/packages/hono/test/bun/middleware.test.ts +++ b/packages/hono/test/bun/middleware.test.ts @@ -26,14 +26,14 @@ vi.mock('@sentry/core', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore applySdkMetadata: vi.fn(actual.applySdkMetadata), - getClient: vi.fn(() => undefined), + _INTERNAL_getActiveClient: vi.fn(() => undefined), // Pass-through so console.warn calls inside consoleSandbox are observable in tests consoleSandbox: vi.fn((cb: () => unknown) => cb()), }; }); const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; -const getClientMock = SentryCore.getClient as Mock; +const getActiveClientMock = SentryCore._INTERNAL_getActiveClient as Mock; describe('Hono Bun Middleware', () => { beforeEach(() => { @@ -64,7 +64,7 @@ describe('Hono Bun Middleware', () => { }); it('calls init from @sentry/bun when no client exists yet', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); const options = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -173,7 +173,7 @@ describe('Hono Bun Middleware', () => { describe('double-init guard', () => { it('does not re-initialize when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -183,7 +183,7 @@ describe('Hono Bun Middleware', () => { it('returns the existing client when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); expect(init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' })).toBe(fakeClient); expect(initBunMock).not.toHaveBeenCalled(); @@ -192,7 +192,7 @@ describe('Hono Bun Middleware', () => { it('emits a console.warn directing to remove the duplicate init call when Sentry is already initialized', () => { const warnSpy = vi.spyOn(console, 'warn'); const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -204,7 +204,7 @@ describe('Hono Bun Middleware', () => { it('does not emit a console.warn when no client exists yet', () => { const warnSpy = vi.spyOn(console, 'warn'); - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -213,7 +213,7 @@ describe('Hono Bun Middleware', () => { }); it('always calls init regardless of whether a client already exists', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); diff --git a/packages/hono/test/deno/middleware.test.ts b/packages/hono/test/deno/middleware.test.ts index 84506ab494fa..3da909433d72 100644 --- a/packages/hono/test/deno/middleware.test.ts +++ b/packages/hono/test/deno/middleware.test.ts @@ -26,14 +26,14 @@ vi.mock('@sentry/core', async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore applySdkMetadata: vi.fn(actual.applySdkMetadata), - getClient: vi.fn(() => undefined), + _INTERNAL_getActiveClient: vi.fn(() => undefined), // Pass-through so console.warn calls inside consoleSandbox are observable in tests consoleSandbox: vi.fn((cb: () => unknown) => cb()), }; }); const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; -const getClientMock = SentryCore.getClient as Mock; +const getActiveClientMock = SentryCore._INTERNAL_getActiveClient as Mock; describe('Hono Deno Middleware', () => { beforeEach(() => { @@ -64,7 +64,7 @@ describe('Hono Deno Middleware', () => { }); it('calls init from @sentry/deno when no client exists yet', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); const options = { dsn: 'https://public@dsn.ingest.sentry.io/1337', @@ -173,7 +173,7 @@ describe('Hono Deno Middleware', () => { describe('double-init guard', () => { it('does not re-initialize when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -183,7 +183,7 @@ describe('Hono Deno Middleware', () => { it('returns the existing client when Sentry is already initialized', () => { const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); expect(init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' })).toBe(fakeClient); expect(initDenoMock).not.toHaveBeenCalled(); @@ -192,7 +192,7 @@ describe('Hono Deno Middleware', () => { it('emits a console.warn directing to remove the duplicate init call when Sentry is already initialized', () => { const warnSpy = vi.spyOn(console, 'warn'); const fakeClient = { getOptions: () => ({}) }; - getClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); + getActiveClientMock.mockReturnValue(fakeClient as unknown as SentryCore.Client); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -204,7 +204,7 @@ describe('Hono Deno Middleware', () => { it('does not emit a console.warn when no client exists yet', () => { const warnSpy = vi.spyOn(console, 'warn'); - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); const app = new Hono(); sentry(app, { dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -213,7 +213,7 @@ describe('Hono Deno Middleware', () => { }); it('always calls init regardless of whether a client already exists', () => { - getClientMock.mockReturnValue(undefined); + getActiveClientMock.mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); diff --git a/packages/hono/test/node/middleware.test.ts b/packages/hono/test/node/middleware.test.ts index 4355fb10e2e8..307eefe05500 100644 --- a/packages/hono/test/node/middleware.test.ts +++ b/packages/hono/test/node/middleware.test.ts @@ -127,8 +127,8 @@ describe('Hono Node Middleware', () => { describe('double-init guard', () => { it('skips re-initialization when a client already exists', () => { const fakeClient = { getOptions: () => ({}) }; - const getClientSpy = vi - .spyOn(SentryCore, 'getClient') + const getActiveClientSpy = vi + .spyOn(SentryCore, '_INTERNAL_getActiveClient') .mockReturnValue(fakeClient as unknown as SentryCore.Client); const result = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -136,17 +136,17 @@ describe('Hono Node Middleware', () => { expect(result).toBe(fakeClient); expect(initNodeMock).not.toHaveBeenCalled(); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); it('initializes normally when no client exists yet', () => { - const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + const getActiveClientSpy = vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); expect(initNodeMock).toHaveBeenCalledTimes(1); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); }); @@ -208,8 +208,8 @@ describe('Hono Node Middleware', () => { describe('double-init guard', () => { it('skips re-initialization when a client already exists', () => { const fakeClient = { getOptions: () => ({}) }; - const getClientSpy = vi - .spyOn(SentryCore, 'getClient') + const getActiveClientSpy = vi + .spyOn(SentryCore, '_INTERNAL_getActiveClient') .mockReturnValue(fakeClient as unknown as SentryCore.Client); const result = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -218,17 +218,17 @@ describe('Hono Node Middleware', () => { expect(initNodeMock).not.toHaveBeenCalled(); expect(applySdkMetadataMock).not.toHaveBeenCalled(); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); it('initializes normally when no client exists yet', () => { - const getClientSpy = vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + const getActiveClientSpy = vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(undefined); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); expect(initNodeMock).toHaveBeenCalledTimes(1); - getClientSpy.mockRestore(); + getActiveClientSpy.mockRestore(); }); }); }); diff --git a/packages/hono/test/node/sdk.test.ts b/packages/hono/test/node/sdk.test.ts index c85b7c951b36..d5045d7ff343 100644 --- a/packages/hono/test/node/sdk.test.ts +++ b/packages/hono/test/node/sdk.test.ts @@ -26,7 +26,7 @@ const applySdkMetadataMock = SentryCore.applySdkMetadata as Mock; describe('Hono Node SDK – init()', () => { beforeEach(() => { vi.clearAllMocks(); - vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(undefined); }); // ─── Happy path ─────────────────────────────────────────────────────────── @@ -136,7 +136,7 @@ describe('Hono Node SDK – init()', () => { it('returns the existing client without re-initializing when already set up', () => { const existingClient = { getOptions: () => ({ debug: false }) }; - vi.spyOn(SentryCore, 'getClient').mockReturnValue(existingClient as unknown as SentryCore.Client); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(existingClient as unknown as SentryCore.Client); const result = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -148,7 +148,7 @@ describe('Hono Node SDK – init()', () => { it('logs a debug message when skipping re-initialization', () => { const logSpy = vi.spyOn(SentryCore.debug, 'log'); const existingClient = { getOptions: () => ({ debug: true }) }; - vi.spyOn(SentryCore, 'getClient').mockReturnValue(existingClient as unknown as SentryCore.Client); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(existingClient as unknown as SentryCore.Client); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -158,7 +158,7 @@ describe('Hono Node SDK – init()', () => { it('does not log when debug is false and skipping re-initialization', () => { const logSpy = vi.spyOn(SentryCore.debug, 'log'); const existingClient = { getOptions: () => ({ debug: false }) }; - vi.spyOn(SentryCore, 'getClient').mockReturnValue(existingClient as unknown as SentryCore.Client); + vi.spyOn(SentryCore, '_INTERNAL_getActiveClient').mockReturnValue(existingClient as unknown as SentryCore.Client); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 37763c0e0607..6f43a282df11 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -2,7 +2,7 @@ // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ import type { Client, EventProcessor, Integration } from '@sentry/core'; -import { addEventProcessor, applySdkMetadata, consoleSandbox, getGlobalScope, GLOBAL_OBJ } from '@sentry/core'; +import { applySdkMetadata, consoleSandbox, getGlobalScope, GLOBAL_OBJ } from '@sentry/core'; import type { BrowserOptions } from '@sentry/react'; import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -22,8 +22,6 @@ export { captureUnderscoreErrorException } from '../common/pages-router-instrume export { browserTracingIntegration } from './browserTracingIntegration'; export { captureRouterTransitionStart } from './routing/appRouterRoutingInstrumentation'; -let clientIsInitialized = false; - const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesAssetPrefixPath: string; _sentryAssetPrefix?: string; @@ -37,16 +35,6 @@ declare const __SENTRY_TRACING__: boolean; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ export function init(options: BrowserOptions): Client | undefined { - if (clientIsInitialized) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] You are calling `Sentry.init()` more than once on the client. This can happen if you have both a `sentry.client.config.ts` and a `instrumentation-client.ts` file with `Sentry.init()` calls. It is recommended to call `Sentry.init()` once in `instrumentation-client.ts`.', - ); - }); - } - clientIsInitialized = true; - if (!DEBUG_BUILD && options.debug) { consoleSandbox(() => { // eslint-disable-next-line no-console @@ -88,10 +76,10 @@ export function init(options: BrowserOptions): Client | undefined { ? null : event; filterNextRedirectError.id = 'NextRedirectErrorFilter'; - addEventProcessor(filterNextRedirectError); + client?.addEventProcessor(filterNextRedirectError); if (process.env.NODE_ENV === 'development') { - addEventProcessor(devErrorSymbolicationEventProcessor); + client?.addEventProcessor(devErrorSymbolicationEventProcessor); } try { diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index d7ce162236fb..4b486c4133d9 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -4,9 +4,9 @@ import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; import type { EventProcessor } from '@sentry/core'; import { + _INTERNAL_getActiveClient, applySdkMetadata, debug, - getClient, getGlobalScope, getRootSpan, getVercelEnv, @@ -182,9 +182,10 @@ export function init(options: NodeOptions): NodeClient | undefined { DEBUG_BUILD && debug.log('Initializing SDK...'); - if (sdkAlreadyInitialized()) { + const existingClient = _INTERNAL_getActiveClient(); + if (existingClient) { DEBUG_BUILD && debug.log('SDK already initialized'); - return; + return existingClient; } // Use appropriate SDK metadata based on the runtime environment @@ -231,7 +232,9 @@ export function init(options: NodeOptions): NodeClient | undefined { client?.on('spanEnd', maybeCompleteCronCheckIn); client?.on('spanEnd', maybeCleanupQueueSpan); - getGlobalScope().addEventProcessor( + // On the client, not the global scope, so a later `init()` after + // `close()` does not stack another copy. + client?.addEventProcessor( Object.assign( ((event, hint) => { if (event.type !== undefined) { @@ -278,7 +281,7 @@ export function init(options: NodeOptions): NodeClient | undefined { }); if (process.env.NODE_ENV === 'development') { - getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); + client?.addEventProcessor(devErrorSymbolicationEventProcessor); } try { @@ -297,10 +300,6 @@ export function init(options: NodeOptions): NodeClient | undefined { return client; } -function sdkAlreadyInitialized(): boolean { - return !!getClient(); -} - export * from '../common'; export { wrapApiHandlerWithSentry } from '../common/pages-router-instrumentation/wrapApiHandlerWithSentry'; diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index f9367080f699..03cd83dbe8b6 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,7 +1,7 @@ import type { Integration } from '@sentry/core'; import { debug, getMainCarrier, GLOBAL_OBJ, SentryNonRecordingSpan, spanToJSON } from '@sentry/core'; import * as SentryReact from '@sentry/react'; -import { getClient, WINDOW } from '@sentry/react'; +import { close, getClient, getIsolationScope, WINDOW } from '@sentry/react'; import { JSDOM } from 'jsdom'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { breadcrumbsIntegration, browserTracingIntegration, init } from '../src/client'; @@ -243,6 +243,22 @@ describe('Client init()', () => { expect(init({})).not.toBeUndefined(); }); + it('registers its event processors on the client', () => { + const client = init({}); + + expect(client?.getEventProcessors().map(processor => processor.id)).toContain('NextRedirectErrorFilter'); + }); + + it('does not add event processors to the isolation scope after close()', async () => { + const isolationProcessorCount = getIsolationScope().getScopeData().eventProcessors.length; + + init({}); + await close(); + init({}); + + expect(getIsolationScope().getScopeData().eventProcessors.length).toBe(isolationProcessorCount); + }); + describe('environment option', () => { const originalEnv = process.env.SENTRY_ENVIRONMENT; const originalNodeEnv = process.env.NODE_ENV; diff --git a/packages/nextjs/test/edgeSdk.test.ts b/packages/nextjs/test/edgeSdk.test.ts index 8d4fee1f926e..0ec8d196a9a3 100644 --- a/packages/nextjs/test/edgeSdk.test.ts +++ b/packages/nextjs/test/edgeSdk.test.ts @@ -20,6 +20,7 @@ describe('Edge init()', () => { }); afterEach(() => { + SentryVercelEdge.getCurrentScope().setClient(undefined); vi.clearAllMocks(); }); diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 25ba979026b3..dfee68309be9 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,6 +1,6 @@ import type { Integration } from '@sentry/core'; import { GLOBAL_OBJ, getMainCarrier } from '@sentry/core'; -import { getCurrentScope } from '@sentry/node'; +import { close, getCurrentScope, getGlobalScope } from '@sentry/node'; import * as SentryNode from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../src/common/span-attributes-with-logic-attached'; @@ -67,6 +67,30 @@ describe('Server init()', () => { expect(nodeInit).toHaveBeenCalledTimes(1); }); + it('registers its event processors on the client', () => { + const client = init({}); + + expect(client?.getEventProcessors().map(processor => processor.id)).toContain('DropReactControlFlowErrors'); + }); + + it('does not add event processors to the global scope after close()', async () => { + const globalProcessorCount = getGlobalScope().getScopeData().eventProcessors.length; + + init({}); + await close(); + init({}); + + expect(getGlobalScope().getScopeData().eventProcessors.length).toBe(globalProcessorCount); + }); + + it('returns the existing client if already initialized', () => { + const first = init({}); + const second = init({}); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + // TODO: test `vercel` tag when running on Vercel // Can't just add the test and set env variables, since the value in `index.server.ts` // is resolved when importing. @@ -154,6 +178,16 @@ describe('Server init()', () => { }); }); + it('initializes again after the existing client is closed', async () => { + const first = init({}); + await first?.close(); + const second = init({}); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(second).toBeDefined(); + expect(second).not.toBe(first); + }); + it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index e4a9e64c6185..57cf3871256a 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -1,5 +1,6 @@ import type { Integration, Options } from '@sentry/core'; import { + _INTERNAL_warnIfClientIsActive, applySdkMetadata, consoleSandbox, conversationIdIntegration, @@ -162,6 +163,8 @@ function _init( const clientOptions = getClientOptions({ ...options, defaultIntegrations }, getDefaultIntegrationsImpl); + _INTERNAL_warnIfClientIsActive(); + const scope = getCurrentScope(); scope.update(clientOptions.initialScope); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 3a9af657e3d9..1063c856c21a 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -1,9 +1,9 @@ import type { Integration } from '@sentry/core'; -import { debug, SDK_VERSION } from '@sentry/core'; +import { debug, originalConsoleMethods, SDK_VERSION } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; import * as SentryServerUtils from '@sentry/server-utils'; import { afterEach, beforeEach, describe, expect, it, type Mock, type MockInstance, vi } from 'vitest'; -import { getClient, NodeClient } from '../../src/'; +import { close, getClient, NodeClient } from '../../src/'; import { init } from '../../src/sdk'; import { cleanupOtel } from '../helpers/mockSdkInit'; @@ -283,6 +283,45 @@ describe('init()', () => { expect(client).toBeInstanceOf(NodeClient); }); + describe('when called again', () => { + // `consoleSandbox` calls the method stored in `originalConsoleMethods`, so a + // spy on `console.warn` misses the warning once the console is instrumented. + const originalWarn = originalConsoleMethods.warn; + let warnSpy: Mock; + + beforeEach(() => { + warnSpy = vi.fn(); + originalConsoleMethods.warn = warnSpy; + }); + + afterEach(() => { + if (originalWarn) { + originalConsoleMethods.warn = originalWarn; + } else { + delete originalConsoleMethods.warn; + } + }); + + it('warns and replaces the active client', () => { + const first = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + const second = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`Sentry.init()` was called more than once')); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + + it('does not warn after close()', async () => { + init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + await close(); + const second = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + }); + it('registers a SIGTERM handler on Vercel', () => { const originalVercelEnv = process.env.VERCEL; process.env.VERCEL = '1'; diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index ec309246f176..0fc7abad204d 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,13 +1,12 @@ import * as path from 'node:path'; import type { Client, Event, EventProcessor } from '@sentry/core'; import { + _INTERNAL_getActiveClient, applySdkMetadata, consoleSandbox, debug, DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT, - getClient, - getGlobalScope, } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -34,14 +33,16 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { // Since the server config is bundled into the Nitro build, a `node --import` preload of a config // file initializes the SDK a second time. The first init wins so a preload keeps its semantics. - if (isNuxtServerInitialized()) { + // A closed client does not count, so `close()` lets a later init set up a new one. + const existingClient = _INTERNAL_getActiveClient(); + if (isNuxtServerInitialized() && existingClient) { consoleSandbox(() => { // eslint-disable-next-line no-console console.log( '[Sentry] The Sentry server SDK is already initialized, skipping a second initialization. The Sentry server config is bundled into the Nitro server build, so a `node --import` preload of the config file is no longer needed and can be removed.', ); }); - return getClient(); + return existingClient; } let isDevBuild = false; @@ -66,8 +67,10 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { markNuxtServerInitialized(); } - getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); - getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); + // On the client, not the global scope, so a later `init()` after + // `close()` does not stack another copy. + client?.addEventProcessor(lowQualityTransactionsFilter(options)); + client?.addEventProcessor(clientSourceMapErrorFilter(options)); return client; } diff --git a/packages/nuxt/test/client/sdk.test.ts b/packages/nuxt/test/client/sdk.test.ts index af285e4adedc..0f9ea837853f 100644 --- a/packages/nuxt/test/client/sdk.test.ts +++ b/packages/nuxt/test/client/sdk.test.ts @@ -9,6 +9,7 @@ describe('Nuxt Client SDK', () => { describe('init', () => { beforeEach(() => { vi.clearAllMocks(); + SentryBrowser.getCurrentScope().setClient(undefined); }); it('Adds Nuxt metadata to the SDK options', () => { diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index ba2c5f82818c..872dc5aeaaa3 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,7 +1,7 @@ -import type { Event, EventProcessor } from '@sentry/core'; +import type { Event } from '@sentry/core'; import { originalConsoleMethods } from '@sentry/core'; import * as SentryNode from '@sentry/node'; -import { getGlobalScope, Scope, SDK_VERSION } from '@sentry/node'; +import { getGlobalScope, SDK_VERSION } from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { NUXT_DEV_MODE_FLAG, NUXT_PRERENDER_FLAG, NUXT_SERVER_INITIALIZED_FLAG } from '../../src/common/devMode'; import { init } from '../../src/server'; @@ -16,6 +16,7 @@ describe('Nuxt Server SDK', () => { // Each test needs a fresh init; the double-init guard would otherwise skip every later call. delete (globalThis as { __SENTRY_NUXT_SERVER_INITIALIZED__?: boolean }).__SENTRY_NUXT_SERVER_INITIALIZED__; delete (globalThis as { __SENTRY_NUXT_PRERENDER__?: boolean }).__SENTRY_NUXT_PRERENDER__; + SentryNode.getCurrentScope().setClient(undefined); }); it('Adds Nuxt metadata to the SDK options', () => { @@ -82,6 +83,26 @@ describe('Nuxt Server SDK', () => { } }); + it('initializes again after close()', async () => { + const firstClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + await SentryNode.close(); + const secondClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(secondClient).toBeDefined(); + expect(secondClient).not.toBe(firstClient); + }); + + it('initializes again after the existing client is closed', async () => { + const firstClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + await firstClient?.close(); + const secondClient = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(secondClient).toBeDefined(); + expect(secondClient).not.toBe(firstClient); + }); + it('marks a successful initialization for the double-init guard', () => { init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); @@ -256,22 +277,24 @@ describe('Nuxt Server SDK', () => { }); }); - it('registers an event processor', async () => { - let passedEventProcessors: EventProcessor[] = []; - const addEventProcessor = vi - .spyOn(getGlobalScope(), 'addEventProcessor') - .mockImplementation((eventProcessor: EventProcessor) => { - passedEventProcessors = [...passedEventProcessors, eventProcessor]; - return new Scope(); - }); - - init({ + it('registers its event processors on the client', () => { + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', }); - expect(addEventProcessor).toHaveBeenCalledTimes(2); - expect(passedEventProcessors[0]?.id).toEqual('NuxtLowQualityTransactionsFilter'); - expect(passedEventProcessors[1]?.id).toEqual('NuxtClientSourceMapErrorFilter'); + const ids = client?.getEventProcessors().map(processor => processor.id); + expect(ids).toContain('NuxtLowQualityTransactionsFilter'); + expect(ids).toContain('NuxtClientSourceMapErrorFilter'); + }); + + it('does not add event processors to the global scope after close()', async () => { + const globalProcessorCount = getGlobalScope().getScopeData().eventProcessors.length; + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + await SentryNode.close(); + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + expect(getGlobalScope().getScopeData().eventProcessors.length).toBe(globalProcessorCount); }); }); diff --git a/packages/react/test/normalize.test.ts b/packages/react/test/normalize.test.ts index 994b6da96919..84fac26ffcf6 100644 --- a/packages/react/test/normalize.test.ts +++ b/packages/react/test/normalize.test.ts @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import { normalize, setNormalizeStringifier } from '@sentry/core'; +import { getCurrentScope, normalize, setNormalizeStringifier } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { init } from '../src/sdk'; @@ -23,6 +23,7 @@ describe('@sentry/react init() normalize stringifier', () => { afterEach(() => { setNormalizeStringifier(undefined); + getCurrentScope().setClient(undefined); }); it("collapses React SyntheticEvent-like objects to '[SyntheticEvent]'", () => { diff --git a/packages/react/test/sdk.test.ts b/packages/react/test/sdk.test.ts index 1fd3d6ade5d1..8cd0bc9c1bc1 100644 --- a/packages/react/test/sdk.test.ts +++ b/packages/react/test/sdk.test.ts @@ -1,9 +1,13 @@ import * as SentryBrowser from '@sentry/browser'; import { version } from 'react'; -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../src/sdk'; describe('init', () => { + afterEach(() => { + SentryBrowser.getCurrentScope().setClient(undefined); + }); + it('sets the React version (if available) in the global scope', () => { const setContextSpy = vi.spyOn(SentryBrowser, 'setContext'); diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index ca31c9f3894d..0f5389854ff0 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -556,5 +556,10 @@ export function instrumentServer(options?: { instrumentTracing?: boolean }): voi return; } + // `init()` runs this again after `close()`, so skip a handler that is already wrapped. + if ((pkg.createRequestHandler as WrappedFunction).__sentry_original__) { + return; + } + fill(pkg, 'createRequestHandler', makeWrappedCreateRequestHandler(options)); } diff --git a/packages/remix/src/server/sdk.ts b/packages/remix/src/server/sdk.ts index 49a10e2065c8..95b2922ddd96 100644 --- a/packages/remix/src/server/sdk.ts +++ b/packages/remix/src/server/sdk.ts @@ -1,7 +1,7 @@ import type { Integration } from '@sentry/core'; -import { applySdkMetadata, debug } from '@sentry/core'; +import { _INTERNAL_getActiveClient, applySdkMetadata, debug } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit } from '@sentry/node'; import { DEBUG_BUILD } from '../utils/debug-build'; import type { RemixOptions } from '../utils/remixOptions'; import { instrumentServer } from './instrumentServer'; @@ -25,10 +25,11 @@ export function getRemixDefaultIntegrations(options: RemixOptions): Integration[ export function init(options: RemixOptions): NodeClient | undefined { applySdkMetadata(options, 'remix', ['remix', 'node']); - if (isInitialized()) { + const existingClient = _INTERNAL_getActiveClient(); + if (existingClient) { DEBUG_BUILD && debug.log('SDK already initialized'); - return; + return existingClient; } options.defaultIntegrations = getRemixDefaultIntegrations(options); diff --git a/packages/remix/test/index.client.test.ts b/packages/remix/test/index.client.test.ts index 1e0e26343494..70e8751496df 100644 --- a/packages/remix/test/index.client.test.ts +++ b/packages/remix/test/index.client.test.ts @@ -1,4 +1,4 @@ -import { getMainCarrier } from '@sentry/core'; +import { getCurrentScope, getMainCarrier } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import { afterEach, describe, expect, it, type Mock, vi } from 'vitest'; import { init } from '../src/index.client'; @@ -49,6 +49,7 @@ describe('Client init()', () => { expect.objectContaining({ routeProvider: expect.objectContaining({ resolveRoute: expect.any(Function) }) }), ); + getCurrentScope().setClient(undefined); const routeProvider = { resolveRoute: () => '/custom', resolveCurrentRoute: () => '/custom' }; init({ routeProvider }); expect(reactInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider })); diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index a6883b986e57..dabba59e4956 100644 --- a/packages/remix/test/index.server.test.ts +++ b/packages/remix/test/index.server.test.ts @@ -48,6 +48,24 @@ describe('Server init()', () => { expect(nodeInit).toHaveBeenCalledTimes(1); }); + it('returns the existing client if already initialized', () => { + const first = init({}); + const second = init({}); + + expect(first).toBeDefined(); + expect(second).toBe(first); + }); + + it('initializes again after the existing client is closed', async () => { + const first = init({}); + await first?.close(); + const second = init({}); + + expect(nodeInit).toHaveBeenCalledTimes(2); + expect(second).toBeDefined(); + expect(second).not.toBe(first); + }); + it('returns client from init', () => { expect(init({})).not.toBeUndefined(); }); diff --git a/packages/remix/test/server/instrumentServer.test.ts b/packages/remix/test/server/instrumentServer.test.ts index b26ef66e3cf1..504c8a17acae 100644 --- a/packages/remix/test/server/instrumentServer.test.ts +++ b/packages/remix/test/server/instrumentServer.test.ts @@ -9,8 +9,29 @@ import { import { FUNCTION } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import * as SentryCore from '@sentry/core'; +import * as SentryCoreServer from '@sentry/core/server'; +import type { WrappedFunction } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { instrumentBuild } from '../../src/server/instrumentServer'; +import { instrumentBuild, instrumentServer } from '../../src/server/instrumentServer'; + +describe('instrumentServer', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('wraps `createRequestHandler` only once when called again', () => { + const createRequestHandler = vi.fn(); + const pkg = { createRequestHandler }; + vi.spyOn(SentryCoreServer, 'loadModule').mockReturnValue(pkg); + + instrumentServer(); + const wrapped = pkg.createRequestHandler; + instrumentServer(); + + expect(pkg.createRequestHandler).toBe(wrapped); + expect((pkg.createRequestHandler as WrappedFunction).__sentry_original__).toBe(createRequestHandler); + }); +}); describe('instrumentBuild', () => { afterEach(() => { diff --git a/packages/replay-internal/test/mocks/mockSdk.ts b/packages/replay-internal/test/mocks/mockSdk.ts index 9ec20553529b..d4965874b0c9 100644 --- a/packages/replay-internal/test/mocks/mockSdk.ts +++ b/packages/replay-internal/test/mocks/mockSdk.ts @@ -1,4 +1,5 @@ import type { Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { vi } from 'vitest'; import type { Replay as ReplayIntegration } from '../../src/integration'; import type { ReplayContainer } from '../../src/replay'; @@ -71,6 +72,8 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true } ...replayOptions, }); + getCurrentScope().setClient(undefined); + const client = init({ ...getDefaultClientOptions(), dsn: 'https://dsn@ingest.f00.f00/1', diff --git a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts index 9eea2d8b03b8..4f03a86e835d 100644 --- a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts +++ b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts @@ -8,6 +8,7 @@ import { createTransport, getActiveSpan, getAsyncContextStrategy, + getCurrentScope, getDefaultCurrentScope, getDefaultIsolationScope, getMainCarrier, @@ -42,6 +43,7 @@ class TestClient extends Client { } function initTestClient(traceLifecycle: 'static' | 'stream' = 'static'): void { + getCurrentScope().setClient(undefined); initAndBind(TestClient, { dsn: 'https://username@domain/123', integrations: [], diff --git a/packages/server-utils/test/integrations/prisma.test.ts b/packages/server-utils/test/integrations/prisma.test.ts index fba6e1706aa1..2d02718b6d1e 100644 --- a/packages/server-utils/test/integrations/prisma.test.ts +++ b/packages/server-utils/test/integrations/prisma.test.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { Client, createTransport, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core'; +import { Client, createTransport, getCurrentScope, initAndBind, resolvedSyncPromise, spanToJSON } from '@sentry/core'; import { afterEach, describe, expect, it } from 'vitest'; import { instrumentPrisma } from '../../src/integrations/prisma'; import type { TracingHelper } from '../../src/integrations/prisma/types'; @@ -23,6 +23,7 @@ class TestClient extends Client { } function initTestClient(): void { + getCurrentScope().setClient(undefined); initAndBind(TestClient, { dsn: 'https://username@domain/123', integrations: [], diff --git a/packages/solidstart/test/client/sdk.test.ts b/packages/solidstart/test/client/sdk.test.ts index 80762894dcb4..9dc280cefc0f 100644 --- a/packages/solidstart/test/client/sdk.test.ts +++ b/packages/solidstart/test/client/sdk.test.ts @@ -9,6 +9,7 @@ const browserInit = vi.spyOn(SentrySolid, 'init'); describe('Initialize Solid Start SDK', () => { beforeEach(() => { vi.clearAllMocks(); + SentrySolid.getCurrentScope().setClient(undefined); }); it('has the correct metadata', () => { @@ -39,6 +40,10 @@ describe('Initialize Solid Start SDK', () => { }); describe('browserTracingIntegration', () => { + beforeEach(() => { + SentrySolid.getCurrentScope().setClient(undefined); + }); + it('adds the `browserTracingIntegration` when `__SENTRY_TRACING__` is not set', () => { const client = solidStartInit({ dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts index c5df698ed307..067e7a6de818 100644 --- a/packages/solidstart/test/server/sdk.test.ts +++ b/packages/solidstart/test/server/sdk.test.ts @@ -10,6 +10,7 @@ const browserInit = vi.spyOn(SentryNode, 'init'); describe('Initialize Solid Start SDK', () => { beforeEach(() => { vi.clearAllMocks(); + SentryNode.getCurrentScope().setClient(undefined); }); it('has the correct metadata', () => { diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts index f151719bc4a2..140e969eea2d 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -12,6 +12,7 @@ const browserInit = vi.spyOn(SentryBrowser, 'init'); describe('Initialize Svelte SDk', () => { beforeEach(() => { vi.clearAllMocks(); + SentryBrowser.getCurrentScope().setClient(undefined); }); it('has the correct metadata', () => { diff --git a/packages/sveltekit/test/client/fetch.test.ts b/packages/sveltekit/test/client/fetch.test.ts index 2819fbbbdeb1..03aa56637899 100644 --- a/packages/sveltekit/test/client/fetch.test.ts +++ b/packages/sveltekit/test/client/fetch.test.ts @@ -3,10 +3,12 @@ */ import { beforeEach, describe, expect, it } from 'vitest'; -import { init } from '../../src/client/index'; +import { getCurrentScope, init } from '../../src/client/index'; describe('instruments fetch', () => { beforeEach(() => { + getCurrentScope().setClient(undefined); + // For the happy path, we can assume that both fetch and the fetch proxy are set // We test the edge cases in the other tests below diff --git a/packages/tanstackstart-react/test/client/sdk.test.ts b/packages/tanstackstart-react/test/client/sdk.test.ts index 0364ee45a6aa..2cb77601eae8 100644 --- a/packages/tanstackstart-react/test/client/sdk.test.ts +++ b/packages/tanstackstart-react/test/client/sdk.test.ts @@ -10,6 +10,7 @@ describe('TanStack Start React Client SDK', () => { beforeEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals(); + SentryReact.getCurrentScope().setClient(undefined); }); it('Adds TanStack Start React client metadata to the SDK options', () => { diff --git a/packages/tanstackstart-react/test/server/sdk.test.ts b/packages/tanstackstart-react/test/server/sdk.test.ts index 889c43fff513..286058bbf512 100644 --- a/packages/tanstackstart-react/test/server/sdk.test.ts +++ b/packages/tanstackstart-react/test/server/sdk.test.ts @@ -9,6 +9,7 @@ describe('TanStack Start React Server SDK', () => { describe('init', () => { beforeEach(() => { vi.clearAllMocks(); + SentryNode.getCurrentScope().setClient(undefined); }); it('Adds TanStack Start React server metadata to the SDK options', () => { diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 70503ad97164..ba3c4ce71830 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -1,6 +1,7 @@ import { diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api'; import type { Client, Integration } from '@sentry/core'; import { + _INTERNAL_warnIfClientIsActive, consoleIntegration, conversationIdIntegration, createStackParser, @@ -80,6 +81,8 @@ export function init(options: VercelEdgeOptions = {}): Client { options.traceLifecycle = options.traceLifecycle ?? getTraceLifecycleFromEnv(process.env.SENTRY_TRACE_LIFECYCLE); + _INTERNAL_warnIfClientIsActive(); + const client = new VercelEdgeClient({ ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), diff --git a/packages/vercel-edge/test/sdk.test.ts b/packages/vercel-edge/test/sdk.test.ts index dbb5fd9e02a4..f9c69cdac777 100644 --- a/packages/vercel-edge/test/sdk.test.ts +++ b/packages/vercel-edge/test/sdk.test.ts @@ -1,6 +1,10 @@ -import { describe, expect, it } from 'vitest'; -import { getDefaultIntegrations, init, spanStreamingIntegration } from '../src'; -import { type Event, type Integration } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { close, getClient, getCurrentScope, getDefaultIntegrations, init, spanStreamingIntegration } from '../src'; +import { type Event, type Integration, originalConsoleMethods } from '@sentry/core'; + +afterEach(() => { + getCurrentScope().setClient(undefined); +}); describe('getDefaultIntegrations', () => { it('includes request data collection by default', () => { @@ -74,4 +78,43 @@ describe('init', () => { expect(integrations?.length).toBe(1); expect((integrations?.[0] as MarkedIntegration)?._custom).toBe(true); }); + + describe('when called again', () => { + // `consoleSandbox` calls the method stored in `originalConsoleMethods`, so a + // spy on `console.warn` misses the warning once the console is instrumented. + const originalWarn = originalConsoleMethods.warn; + let warnSpy: Mock; + + beforeEach(() => { + warnSpy = vi.fn(); + originalConsoleMethods.warn = warnSpy; + }); + + afterEach(() => { + if (originalWarn) { + originalConsoleMethods.warn = originalWarn; + } else { + delete originalConsoleMethods.warn; + } + }); + + it('warns and replaces the active client', () => { + const first = init({ enableOpenTelemetrySetup: false }); + const second = init({ enableOpenTelemetrySetup: false }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('`Sentry.init()` was called more than once')); + expect(second).not.toBe(first); + expect(getClient()).toBe(second); + }); + + it('does not warn after close()', async () => { + init({ enableOpenTelemetrySetup: false }); + await close(); + const second = init({ enableOpenTelemetrySetup: false }); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(getClient()).toBe(second); + }); + }); }); diff --git a/packages/vue/test/integration/VueIntegration.test.ts b/packages/vue/test/integration/VueIntegration.test.ts index 6197beeb2d67..3b8180b7a93c 100644 --- a/packages/vue/test/integration/VueIntegration.test.ts +++ b/packages/vue/test/integration/VueIntegration.test.ts @@ -45,6 +45,7 @@ describe('Sentry.VueIntegration', () => { }); afterEach(() => { + Sentry.getCurrentScope().setClient(undefined); vi.resetAllMocks(); }); diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index 6a596588335f..20793b05c5fa 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -21,6 +21,7 @@ describe('Sentry.init', () => { }); afterEach(() => { + Sentry.getCurrentScope().setClient(undefined); vi.clearAllMocks(); }); diff --git a/packages/vue/test/integration/mixinRegistration.test.ts b/packages/vue/test/integration/mixinRegistration.test.ts index 90abef41e74b..c9dcf4942c02 100644 --- a/packages/vue/test/integration/mixinRegistration.test.ts +++ b/packages/vue/test/integration/mixinRegistration.test.ts @@ -130,6 +130,7 @@ const it = baseIt.extend({ } }); }); + Sentry.getCurrentScope().setClient(undefined); }, }); /* oxlint-enable no-empty-pattern */ diff --git a/packages/vue/test/integration/normalize.test.ts b/packages/vue/test/integration/normalize.test.ts index 3fdedb5433a9..90764e810960 100644 --- a/packages/vue/test/integration/normalize.test.ts +++ b/packages/vue/test/integration/normalize.test.ts @@ -26,6 +26,7 @@ describe('@sentry/vue init() normalize stringifier', () => { afterEach(() => { // Reset the registered stringifier so subsequent test files start clean. setNormalizeStringifier(undefined); + Sentry.getCurrentScope().setClient(undefined); }); it("collapses Vue 3 component instances (`__isVue`) to '[VueViewModel]'", () => { From b31048bd8f4cf9b5edf2862812967c41d944823d Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 6 Oct 2026 09:21:06 +0200 Subject: [PATCH 32/73] test(browser): port general browser integration suites to default streaming (#24876) Let non-tracing browser integration suites exercise the default span streaming lifecycle instead of pinning static tracing. Tests that also inspect trace data now assert on streamed spans, and the culture/HTTP context twins are consolidated while preserving error-event coverage. The affected Chromium run passed 370 tests with six skipped; its one slow-click failure also reproduces with the original static setup. The rewritten suites passed in Chromium and WebKit, and the full unit suite, build, lint, and formatting checks passed. Fixes #24145 --------- Co-authored-by: GPT-6 --- .../errors/fetch-enhance-messages-off/init.js | 1 - .../init.js | 1 - .../suites/errors/fetch/init.js | 1 - .../suites/feedback/attachTo/init.js | 1 - .../suites/feedback/captureFeedback/init.js | 1 - .../hasSampling/init.js | 1 - .../feedback/captureFeedbackCsp/init.js | 1 - .../captureFeedbackWithProfiling/init.js | 1 - .../captureFeedbackWithProfiling/test.ts | 15 ++- .../suites/feedback/logger/init.js | 1 - .../integrations/Breadcrumbs/console/init.js | 1 - .../integrations/Breadcrumbs/dom/init.js | 1 - .../integrations/Breadcrumbs/fetch/init.js | 1 - .../integrations/Breadcrumbs/history/init.js | 1 - .../integrations/Breadcrumbs/xhr/init.js | 1 - .../suites/integrations/ContextLines/init.js | 1 - .../integrations/browserApiErrors/init.js | 1 - .../unregisterOriginalCallbacks/init.js | 1 - .../captureConsole-attachStackTrace/init.js | 1 - .../integrations/captureConsole/init.js | 1 - .../cultureContext-streamed/init.js | 9 -- .../cultureContext-streamed/test.ts | 21 ---- .../integrations/cultureContext/init.js | 17 +-- .../integrations/cultureContext/subject.js | 2 +- .../integrations/cultureContext/test.ts | 26 ++++- .../featureFlags/featureFlags/onError/init.js | 1 - .../featureFlags/featureFlags/onSpan/init.js | 1 - .../featureFlags/featureFlags/onSpan/test.ts | 34 ++---- .../featureFlags/growthbook/onError/init.js | 1 - .../featureFlags/growthbook/onSpan/init.js | 1 - .../featureFlags/growthbook/onSpan/test.ts | 33 ++---- .../featureFlags/launchdarkly/onError/init.js | 1 - .../featureFlags/launchdarkly/onSpan/init.js | 1 - .../featureFlags/launchdarkly/onSpan/test.ts | 34 ++---- .../openfeature/onError/errorHook/init.js | 1 - .../featureFlags/openfeature/onError/init.js | 1 - .../featureFlags/openfeature/onSpan/init.js | 1 - .../featureFlags/openfeature/onSpan/test.ts | 34 ++---- .../featureFlags/statsig/onError/init.js | 1 - .../featureFlags/statsig/onSpan/init.js | 1 - .../featureFlags/statsig/onSpan/test.ts | 34 ++---- .../featureFlags/unleash/badSignature/init.js | 1 - .../featureFlags/unleash/onError/init.js | 1 - .../featureFlags/unleash/onSpan/init.js | 1 - .../featureFlags/unleash/onSpan/test.ts | 34 ++---- .../integrations/globalHandlers/init.js | 1 - .../integrations/graphqlClient/fetch/test.ts | 52 +++++---- .../suites/integrations/graphqlClient/init.js | 1 - .../persistedQuery-fetch/test.ts | 57 +++++---- .../graphqlClient/persistedQuery-xhr/test.ts | 59 ++++++---- .../integrations/graphqlClient/xhr/test.ts | 54 +++++---- .../integrations/httpContext-streamed/init.js | 9 -- .../httpContext-streamed/subject.js | 5 - .../integrations/httpContext-streamed/test.ts | 50 -------- .../suites/integrations/httpContext/init.js | 17 +-- .../integrations/httpContext/subject.js | 8 +- .../suites/integrations/httpContext/test.ts | 50 +++++++- .../lazyLoad/existingIntegration/init.js | 1 - .../lazyLoad/feedbackIntegration/init.js | 1 - .../moduleMetadataIntegration/init.js | 1 - .../lazyLoad/validIntegration/init.js | 1 - .../lazyLoad/validIntegrationNpm/init.js | 1 - .../moduleMetadata/appliesMetadata/init.js | 1 - .../appliesMetadataWithRewriteFrames/init.js | 1 - .../suites/integrations/supabase/auth/init.js | 1 - .../suites/integrations/supabase/auth/test.ts | 108 ++++++++++-------- .../supabase/db-operations/init.js | 1 - .../supabase/db-operations/test.ts | 35 +++--- .../thirdPartyErrorsFilter/init.js | 1 - .../suites/integrations/viewHierarchy/init.js | 1 - .../suites/integrations/webWorker/init.js | 1 - .../suites/ipv6/init.js | 1 - .../force-init-chrome-extension/init.js | 1 - .../skip-init-browser-extension/init.js | 1 - .../skip-init-chrome-extension/init.js | 1 - .../acs/getCurrentScope/init.js | 1 - .../old-sdk-interop/hub/isOlderThan/init.js | 1 - .../suites/public-api/addBreadcrumb/init.js | 1 - .../captureException/errorEvent/init.js | 1 - .../public-api/captureException/init.js | 1 - .../suites/public-api/captureFeedback/init.js | 1 - .../withCaptureException/init.js | 1 - .../withCaptureMessage/init.js | 1 - .../suites/public-api/captureMessage/init.js | 1 - .../multipleMessageAttachStacktrace/init.js | 1 - .../simple_message_attachStackTrace/init.js | 1 - .../suites/public-api/dataCollection/init.js | 1 - .../dataCollection/overrideIp/init.js | 1 - .../dataCollection/performance/test.ts | 23 ++-- .../public-api/dataCollection/replay/init.js | 1 - .../dataCollection/sessions/init.js | 1 - .../suites/public-api/debug/init.js | 1 - .../suites/public-api/denyUrls/init.js | 1 - .../diagnoseSdkConnectivity/init.js | 1 - .../diagnoseSdkConnectivity/test.ts | 31 ++++- .../suites/public-api/ignoreErrors/init.js | 1 - .../suites/public-api/init/init.js | 1 - .../public-api/init/stringSampleRate/init.js | 1 - .../instrumentation/eventListener/init.js | 1 - .../eventListener/original-callback/init.js | 1 - .../suites/public-api/instrumentation/init.js | 1 - .../instrumentation/onError/init.js | 1 - .../onUnhandledRejection/init.js | 1 - .../requestAnimationFrame/init.js | 1 - .../instrumentation/setTimeoutFrozen/init.js | 1 - .../public-api/instrumentation/xhr/init.js | 1 - .../consoleLoggingIntegrationShim/init.js | 1 - .../suites/public-api/logger/init.js | 1 - .../public-api/logger/integration/init.js | 1 - .../public-api/logger/loggerShim/init.js | 1 - .../metrics/afterCaptureMetric/init.js | 1 - .../suites/public-api/metrics/init.js | 1 - .../public-api/metrics/metricsShim/init.js | 1 - .../suites/public-api/setContext/init.js | 1 - .../suites/public-api/setExtra/init.js | 1 - .../suites/public-api/setExtras/init.js | 1 - .../suites/public-api/setTag/init.js | 1 - .../suites/public-api/setTags/init.js | 1 - .../suites/public-api/setUser/init.js | 1 - .../public-api/showReportDialog/init.js | 1 - .../suites/public-api/withScope/init.js | 1 - .../replay/attachRawBodyFromRequest/init.js | 1 - .../suites/replay/autoFlushOnFeedback/init.js | 1 - .../suites/replay/bufferModeManual/init.js | 1 - .../suites/replay/bufferModeReload/init.js | 1 - .../replay/bufferStalledRequests/init.js | 1 - .../replay/canvas/manualSnapshot/init.js | 1 - .../suites/replay/canvas/records/init.js | 1 - .../canvas/withCanvasIntegrationFirst/init.js | 1 - .../withCanvasIntegrationSecond/init.js | 1 - .../canvas/withoutCanvasIntegration/init.js | 1 - .../replay/captureComponentName/init.js | 1 - .../captureReplayFromReplayPackage/init.js | 1 - .../replay/captureReplayOffline/init.js | 1 - .../suites/replay/compressionDisabled/init.js | 1 - .../suites/replay/compressionEnabled/init.js | 1 - .../replay/compressionWorkerUrl/init.js | 1 - .../suites/replay/customEvents/init.js | 1 - .../suites/replay/dsc/init.js | 1 - .../suites/replay/dsc/test.ts | 76 +++++------- .../replay/errors/beforeErrorSampling/init.js | 1 - .../suites/replay/errors/droppedError/init.js | 1 - .../errors/errorModeCustomTransport/init.js | 1 - .../suites/replay/errors/errorNotSent/init.js | 1 - .../replay/errors/errorsInSession/init.js | 1 - .../suites/replay/errors/init.js | 1 - .../fetch/captureRequestBody/init.js | 1 - .../fetch/captureRequestHeaders/init.js | 1 - .../fetch/captureResponseBody/init.js | 1 - .../fetch/captureResponseHeaders/init.js | 1 - .../fetch/captureTimestamps/init.js | 1 - .../extendNetworkBreadcrumbs/fetch/init.js | 1 - .../xhr/captureRequestBody/init.js | 1 - .../xhr/captureRequestHeaders/init.js | 1 - .../xhr/captureResponseBody/init.js | 1 - .../xhr/captureResponseHeaders/init.js | 1 - .../xhr/captureTimestamps/init.js | 1 - .../extendNetworkBreadcrumbs/xhr/init.js | 1 - .../suites/replay/fileInput/init.js | 1 - .../suites/replay/flushing/init.js | 1 - .../suites/replay/ignoreMutations/init.js | 1 - .../suites/replay/init.js | 1 - .../suites/replay/keyboardEvents/init.js | 1 - .../largeMutations/defaultOptions/init.js | 1 - .../largeMutations/mutationLimit/init.js | 1 - .../suites/replay/logger/init.js | 1 - .../suites/replay/maxReplayDuration/init.js | 1 - .../suites/replay/minReplayDuration/init.js | 1 - .../replay/minReplayDurationLimit/init.js | 1 - .../suites/replay/multiple-pages/init.js | 1 - .../suites/replay/privacyBlock/init.js | 1 - .../suites/replay/privacyDefault/init.js | 1 - .../suites/replay/privacyInput/init.js | 1 - .../suites/replay/privacyInputMaskAll/init.js | 1 - .../replay/replayIntegrationShim/init.js | 1 - .../suites/replay/replayShim/init.js | 1 - .../suites/replay/requests/init.js | 1 - .../suites/replay/sampling/init.js | 1 - .../suites/replay/sessionExpiry/init.js | 1 - .../suites/replay/sessionInactive/init.js | 1 - .../suites/replay/sessionMaxAge/init.js | 1 - .../suites/replay/slowClick/disable/init.js | 1 - .../suites/replay/slowClick/error/init.js | 1 - .../suites/replay/slowClick/init.js | 1 - .../suites/replay/throttleBreadcrumbs/init.js | 1 - .../suites/replay/unicode/compressed/init.js | 1 - .../replay/unicode/uncompressed/init.js | 1 - .../suites/sessions/init.js | 1 - .../suites/sessions/initial-scope/init.js | 1 - .../suites/sessions/page-lifecycle/init.js | 1 - .../suites/sessions/route-lifecycle/init.js | 1 - .../sessions/sampled-session-update/init.js | 1 - .../suites/sessions/start-session/init.js | 1 - .../suites/sessions/user/init.js | 1 - .../suites/stacktraces/init.js | 1 - .../suites/transport/multiplexed/init.js | 1 - .../suites/transport/offline/init.js | 1 - .../suites/wasm/init.js | 1 - .../instantiateBufferRegistration/init.js | 1 - .../suites/wasm/thirdPartyFilter/init.js | 1 - .../suites/wasm/webWorker/init.js | 1 - 201 files changed, 427 insertions(+), 674 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js index 0ca6f3d1f855..783f188b3ba2 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-off/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', enhanceFetchErrorMessages: false, }); diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js index 9de3c3f1bf62..535d3397fb60 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch-enhance-messages-report-only/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', enhanceFetchErrorMessages: 'report-only', }); diff --git a/dev-packages/browser-integration-tests/suites/errors/fetch/init.js b/dev-packages/browser-integration-tests/suites/errors/fetch/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/errors/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/errors/fetch/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js b/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js index be51e911f499..740fb69558ed 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/init.js @@ -10,7 +10,6 @@ window.Sentry = Sentry; window.feedback = feedback; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [feedback], }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js index d918418d502d..e6da8b5973bb 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ feedbackIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js index ec99f339e2c1..c8d5dbcdb232 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', replaysOnErrorSampleRate: 1.0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js index 81b53be75df9..27e5495f66a8 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/init.js @@ -5,7 +5,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ feedbackIntegration({ tags: { from: 'integration init' }, styleNonce: 'foo1234', scriptNonce: 'foo1234' }), diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js index 727d203aadd4..19dbc202c7e1 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/init.js @@ -8,7 +8,6 @@ import { feedbackIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts index ecbb7c3790ec..a9ef9812ff41 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackWithProfiling/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; import { sentryTest } from '../../../utils/fixtures'; import { envelopeRequestParser, getEnvelopeType, shouldSkipFeedbackTest, shouldSkipTracingTest, - waitForTransactionRequest, } from '../../../utils/helpers'; sentryTest( @@ -21,8 +21,8 @@ sentryTest( responseHeaders: { 'Document-Policy': 'js-profiling' }, }); - // Wait for the pageload transaction to be sent (idle span ended) - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + // Wait for the idle pageload span to end. + const pageloadSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const feedbackRequestPromise = page.waitForResponse(res => { const req = res.request(); @@ -40,7 +40,7 @@ sentryTest( await page.goto(url); // Wait for the idle page load span to finish - const pageLoadEvent = envelopeRequestParser(await pageloadRequestPromise); + const pageloadSpan = await pageloadSpanPromise; // Submit feedback after idle span ended — no active span await page.getByText('Report a Bug').waitFor({ state: 'visible' }); @@ -55,11 +55,10 @@ sentryTest( expect(feedbackEvent.contexts?.trace?.trace_id).toMatch(/\w{32}/); expect(feedbackEvent.contexts?.trace?.span_id).toMatch(/\w{16}/); - // contexts.trace.data must include thread.id to identify which thread is associated with the transaction - expect(pageLoadEvent.contexts?.trace?.data?.['thread.id']).toBe('0'); - expect(pageLoadEvent.contexts?.trace?.data?.['thread.name']).toBe('main'); + expect(pageloadSpan.attributes['thread.id']).toEqual({ type: 'string', value: '0' }); + expect(pageloadSpan.attributes['thread.name']).toEqual({ type: 'string', value: 'main' }); - const profilerId = pageLoadEvent.contexts?.profile?.profiler_id; + const profilerId = pageloadSpan.attributes['sentry.profiler_id']?.value; expect(profilerId).toMatch(/^[a-f\d]{32}$/); expect(feedbackEvent.contexts?.profile?.profiler_id).toBe(profilerId); }, diff --git a/dev-packages/browser-integration-tests/suites/feedback/logger/init.js b/dev-packages/browser-integration-tests/suites/feedback/logger/init.js index 46e56c1330e3..3251bd6c7a4c 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/feedback/logger/init.js @@ -10,7 +10,6 @@ window.Sentry = Sentry; window.feedback = feedback; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', debug: true, integrations: [feedback], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js index 50c6a0433fd3..82fa0942064b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/console/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration(), Sentry.consoleIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/fetch/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/history/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js index 8830eefcc1c5..36806d01c6d0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/xhr/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, integrations: [Sentry.breadcrumbsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js index 19eb351a12a9..cdcb68f2b48f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/init.js @@ -4,7 +4,6 @@ import { contextLinesIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [contextLinesIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js index b5214b18f262..eac01ad8ff64 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/init.js @@ -12,7 +12,6 @@ const myClickListener = () => { btn.addEventListener('click', myClickListener); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js index d8f8833ee972..ecf0d4331b0d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks/init.js @@ -12,7 +12,6 @@ const myClickListener = () => { btn.addEventListener('click', myClickListener); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserApiErrorsIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js index 0cc89d7a3328..d3f555f38933 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole-attachStackTrace/init.js @@ -4,7 +4,6 @@ import { captureConsoleIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [captureConsoleIntegration()], attachStacktrace: true, diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js index 47b7cfd820ae..d265bdde9ed3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js @@ -4,7 +4,6 @@ import { captureConsoleIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [captureConsoleIntegration()], attachStacktrace: false, diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js deleted file mode 100644 index c69a872adc77..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/init.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration(), Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts b/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts deleted file mode 100644 index 0d3484ef41bd..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext-streamed/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; - -sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); - - expect(pageloadSpan!.attributes['culture.locale']).toEqual({ type: 'string', value: expect.any(String) }); - expect(pageloadSpan!.attributes['culture.timezone']).toEqual({ type: 'string', value: expect.any(String) }); - expect(pageloadSpan!.attributes['culture.calendar']).toEqual({ type: 'string', value: expect.any(String) }); -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js index 476df71792bd..a408b17f97f8 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/init.js @@ -2,19 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -const integrations = Sentry.getDefaultIntegrations({}).filter( - defaultIntegration => defaultIntegration.name === 'CultureContext', -); - -const client = new Sentry.BrowserClient({ +Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - transport: Sentry.makeFetchTransport, - stackParser: Sentry.defaultStackParser, - integrations: integrations, + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, }); - -const scope = new Sentry.Scope(); -scope.setClient(client); -client.init(); - -window._sentryScope = scope; diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js index 7881f177f9ae..564808cf73f0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/subject.js @@ -1 +1 @@ -window._sentryScope.captureException(new Error('test error')); +window.Sentry.captureException(new Error('test error')); diff --git a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts index a3078b315a92..d3c92273d124 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/cultureContext/test.ts @@ -1,12 +1,14 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('cultureContextIntegration captures locale, timezone, and calendar', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const errorRequestPromise = waitForErrorRequest(page); + await page.goto(url); + const eventData = envelopeRequestParser(await errorRequestPromise); expect(eventData.exception?.values).toHaveLength(1); @@ -16,3 +18,21 @@ sentryTest('cultureContextIntegration captures locale, timezone, and calendar', calendar: expect.any(String), }); }); + +sentryTest( + 'cultureContextIntegration adds locale, timezone, and calendar to spans', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url); + + const pageloadSpan = await spanPromise; + + expect(pageloadSpan.attributes['culture.locale']).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan.attributes['culture.timezone']).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan.attributes['culture.calendar']).toEqual({ type: 'string', value: expect.any(String) }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js index 9bea0bc1b630..894f46aaa102 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; // window.sentryFeatureFlagsIntegration = Sentry.featureFlagsIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [Sentry.featureFlagsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js index f3f8089dffda..fa6a67ec3711 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index ee132346d51c..1ff0935b65f3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); @@ -45,22 +35,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js index f81b5b380a1b..e7831a1c2c0b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js @@ -31,7 +31,6 @@ window.Sentry = Sentry; window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryGrowthBookIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js index f3e059103533..d755d7a1d972 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js @@ -29,7 +29,6 @@ window.Sentry = Sentry; window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts index b93047ed01d3..df64f1c51bec 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest( "GrowthBook onSpan: flags are added to active span's attributes on span end", @@ -23,12 +18,8 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { @@ -44,22 +35,20 @@ sentryTest( return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = [] as Array<[string, unknown]>; + const expectedInnerSpanFlags = [] as Array<[string, unknown]>; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }, ); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js index 5289952f89b5..810539a8c07c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryLDIntegration = Sentry.launchDarklyIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryLDIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js index a16f1729de83..9e4b802f28f3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryLDIntegration = Sentry.launchDarklyIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts index f6f243262f49..3b9eabb860f2 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const ldClient = (window as any).initializeLD(); @@ -45,22 +35,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js index ca57b1550df0..971e08755fe6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryOpenFeatureIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js index 0fad111b5d95..b2b48519b8a9 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryOpenFeatureIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js index eaebe2c27258..4fc1cace150c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts index ff03be5a972b..8dffd14502b7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const client = (window as any).initialize(); @@ -45,22 +35,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js index 1ffa2a176dcd..61d00587f4c6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js @@ -29,7 +29,6 @@ window.Sentry = Sentry; window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryStatsigIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js index 15446bd6e931..22f74d2ebd7c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js @@ -29,7 +29,6 @@ window.Sentry = Sentry; window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts index 34a760a02e61..d78ebe56de88 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const client = (window as any).statsigClient; @@ -49,22 +39,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js index 0599b97ef85f..1e0303b9c356 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js @@ -10,7 +10,6 @@ window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryUnleashIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js index cdfc8753fd55..ddc74b6427b4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js @@ -44,7 +44,6 @@ window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, integrations: [window.sentryUnleashIntegration], diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js index f75906993803..93993d8f6188 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js @@ -49,7 +49,6 @@ window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts index cf92c32f2a8f..12e31d3ac568 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -1,13 +1,8 @@ import { expect } from '@playwright/test'; import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { waitForStreamedSpan } from '../../../../../utils/spanUtils'; import { sentryTest } from '../../../../../utils/fixtures'; -import { - type EventAndTraceHeader, - eventAndTraceHeaderRequestParser, - getMultipleSentryEnvelopeRequests, - shouldSkipFeatureFlagsTest, - shouldSkipTracingTest, -} from '../../../../../utils/helpers'; +import { shouldSkipFeatureFlagsTest, shouldSkipTracingTest } from '../../../../../utils/helpers'; sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { @@ -25,14 +20,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); - const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - {}, - eventAndTraceHeaderRequestParser, - ); + const innerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-nested-span'); + const outerSpanPromise = waitForStreamedSpan(page, span => span.name === 'test-span'); - // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. await page.evaluate(maxFlags => { (window as any).withNestedSpans(() => { const client = new (window as any).UnleashClient(); @@ -47,22 +37,20 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a return true; }, MAX_FLAGS_PER_SPAN); - const event = (await envelopeRequestPromise)[0][0]; - const innerSpan = event.spans?.[0]; - const outerSpan = event.spans?.[1]; - const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + const [innerSpan, outerSpan] = await Promise.all([innerSpanPromise, outerSpanPromise]); + const outerSpanFlags = Object.entries(outerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + const innerSpanFlags = Object.entries(innerSpan.attributes).filter(([key, _val]) => key.startsWith('flag.evaluation'), ); - expect(innerSpanFlags).toEqual([]); + expect(outerSpanFlags).toEqual([]); - const expectedOuterSpanFlags = []; + const expectedInnerSpanFlags = []; for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + expectedInnerSpanFlags.push([`flag.evaluation.feat${i}`, { type: 'boolean', value: i === 3 }]); } // Order agnostic (attribute dict is unordered). - expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); + expect(innerSpanFlags.sort()).toEqual(expectedInnerSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/globalHandlers/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts index 98bdc15e6d4e..7337b2e089a6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/fetch/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; // Duplicate from subject.js const query = `query Test{ @@ -13,7 +13,7 @@ const query = `query Test{ sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -33,34 +33,34 @@ sentryTest('should update spans for GraphQL fetch requests', async ({ getLocalTe }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/foo (query Test)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - type: 'fetch', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/foo', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.document': query, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/foo' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.document': { type: 'string', value: query }, }), }); }); sentryTest('should update breadcrumbs for GraphQL fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -80,7 +80,15 @@ sentryTest('should update breadcrumbs for GraphQL fetch requests', async ({ getL }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js index c7bd45083a80..ef8d5fa541e4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/init.js @@ -5,7 +5,6 @@ import { graphqlClientIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts index 85bca8b11546..c2b617049695 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-fetch/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; sentryTest('should update spans for GraphQL persisted query fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -27,28 +27,31 @@ sentryTest('should update spans for GraphQL persisted query fetch requests', asy }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/graphql (persisted GetUser)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - type: 'fetch', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/graphql', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', - 'graphql.persisted_query.version': 1, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/graphql' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.persisted_query.hash.sha256': { + type: 'string', + value: 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + }, + 'graphql.persisted_query.version': { type: 'integer', value: 1 }, }), }); }); @@ -57,7 +60,7 @@ sentryTest( 'should update breadcrumbs for GraphQL persisted query fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -79,7 +82,15 @@ sentryTest( }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts index 7e48cf913934..db89e9bf7f57 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/persistedQuery-xhr/test.ts @@ -1,11 +1,11 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; sentryTest('should update spans for GraphQL persisted query XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -27,35 +27,38 @@ sentryTest('should update spans for GraphQL persisted query XHR requests', async }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/graphql (persisted GetUser)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: { - type: 'xhr', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/graphql', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.persisted_query.hash.sha256': 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', - 'graphql.persisted_query.version': 1, - }, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'xhr' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/graphql' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.persisted_query.hash.sha256': { + type: 'string', + value: 'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38', + }, + 'graphql.persisted_query.version': { type: 'integer', value: 1 }, + }), }); }); sentryTest('should update breadcrumbs for GraphQL persisted query XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -77,7 +80,15 @@ sentryTest('should update breadcrumbs for GraphQL persisted query XHR requests', }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts index 722f6487c072..b46292284e82 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/graphqlClient/xhr/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; // Duplicate from subject.js const query = `query Test{ @@ -13,7 +13,7 @@ const query = `query Test{ sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -33,34 +33,34 @@ sentryTest('should update spans for GraphQL XHR requests', async ({ getLocalTest }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const requestSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); + await page.goto(url); + const [rootSpan, requestSpan] = await Promise.all([rootSpanPromise, requestSpanPromise]); - expect(requestSpans).toHaveLength(1); - - expect(requestSpans![0]).toMatchObject({ - description: 'POST http://sentry-test.io/foo (query Test)', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(requestSpan).toMatchObject({ + name: 'POST sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: { - type: 'xhr', - 'http.request.method': 'POST', - 'url.full': 'http://sentry-test.io/foo', - 'server.address': 'sentry-test.io', - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - 'graphql.document': query, - }, + attributes: expect.objectContaining({ + type: { type: 'string', value: 'xhr' }, + 'http.request.method': { type: 'string', value: 'POST' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/foo' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'graphql.document': { type: 'string', value: query }, + }), }); }); sentryTest('should update breadcrumbs for GraphQL XHR requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); @@ -80,7 +80,15 @@ sentryTest('should update breadcrumbs for GraphQL XHR requests', async ({ getLoc }); }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + await rootSpanPromise; + const eventPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'GraphQL breadcrumb test error', + ); + await page.evaluate(() => (window as any).Sentry.captureException(new Error('GraphQL breadcrumb test error'))); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData?.breadcrumbs?.length).toBe(1); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js deleted file mode 100644 index c69a872adc77..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/init.js +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.spanStreamingIntegration(), Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js deleted file mode 100644 index f1547634fb45..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/subject.js +++ /dev/null @@ -1,5 +0,0 @@ -Sentry.startSpan({ name: 'parent-span', op: 'test' }, () => { - Sentry.startSpan({ name: 'child-span', op: 'test-child' }, () => { - // noop - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts deleted file mode 100644 index d37127a7897a..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext-streamed/test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; -import { URL_FULL, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; - -sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => getSpanOp(s) === 'pageload')); - - await page.goto(url, { referer: 'https://sentry.io/' }); - - const spans = await spansPromise; - - const pageloadSpan = spans.find(s => getSpanOp(s) === 'pageload'); - - expect(pageloadSpan!.attributes[URL_FULL]).toEqual({ type: 'string', value: expect.any(String) }); - expect(pageloadSpan!.attributes[USER_AGENT_ORIGINAL]).toEqual({ - type: 'string', - value: expect.any(String), - }); - expect(pageloadSpan!.attributes['http.request.header.referer']).toEqual({ - type: 'array', - value: ['https://sentry.io/'], - }); -}); - -sentryTest( - 'httpContextIntegration only attaches the user agent to non-segment spans', - async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans(page, spans => spans.some(s => s.name === 'child-span')); - - await page.goto(url, { referer: 'https://sentry.io/' }); - - const spans = await spansPromise; - - const childSpan = spans.find(s => s.name === 'child-span'); - - expect(childSpan!.is_segment).toBe(false); - expect(childSpan!.attributes[USER_AGENT_ORIGINAL]).toEqual({ type: 'string', value: expect.any(String) }); - // The document URL and referer only belong on the segment span. - expect(childSpan!.attributes[URL_FULL]).toBeUndefined(); - expect(childSpan!.attributes['http.request.header.referer']).toBeUndefined(); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js index 984534454ac7..a408b17f97f8 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/init.js @@ -2,19 +2,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; -const integrations = Sentry.getDefaultIntegrations({}).filter( - defaultIntegration => defaultIntegration.name === 'HttpContext', -); - -const client = new Sentry.BrowserClient({ +Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - transport: Sentry.makeFetchTransport, - stackParser: Sentry.defaultStackParser, - integrations: integrations, + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, }); - -const scope = new Sentry.Scope(); -scope.setClient(client); -client.init(); - -window._sentryScope = scope; diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js index 62ce205e2ffc..8abda6d4b6f7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/subject.js @@ -1 +1,7 @@ -window._sentryScope.captureException(new Error('client init')); +Sentry.startSpan({ name: 'parent-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'child-span', op: 'test-child' }, () => { + // noop + }); +}); + +window.Sentry.captureException(new Error('test error')); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts index dd8a870a3bad..db8a384510a4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpContext/test.ts @@ -1,17 +1,18 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { URL_FULL, USER_AGENT_ORIGINAL } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; sentryTest('httpContextIntegration captures user-agent and referrer', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const errorEventPromise = getFirstSentryEnvelopeRequest(page); + const errorEventPromise = waitForErrorRequest(page); // Simulate document.referrer being set to test full functionality of the integration await page.goto(url, { referer: 'https://sentry.io/' }); - const errorEvent = await errorEventPromise; + const errorEvent = envelopeRequestParser(await errorEventPromise); expect(errorEvent.exception?.values).toHaveLength(1); @@ -23,3 +24,44 @@ sentryTest('httpContextIntegration captures user-agent and referrer', async ({ g url: expect.any(String), }); }); + +sentryTest('httpContextIntegration captures url, user-agent, and referer', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url, { referer: 'https://sentry.io/' }); + + const pageloadSpan = await spanPromise; + + expect(pageloadSpan.attributes[URL_FULL]).toEqual({ type: 'string', value: expect.any(String) }); + expect(pageloadSpan.attributes[USER_AGENT_ORIGINAL]).toEqual({ + type: 'string', + value: expect.any(String), + }); + expect(pageloadSpan.attributes['http.request.header.referer']).toEqual({ + type: 'array', + value: ['https://sentry.io/'], + }); +}); + +sentryTest( + 'httpContextIntegration only attaches the user agent to non-segment spans', + async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const url = await getLocalTestUrl({ testDir: __dirname }); + + const spanPromise = waitForStreamedSpan(page, span => span.name === 'child-span'); + + await page.goto(url, { referer: 'https://sentry.io/' }); + + const childSpan = await spanPromise; + + expect(childSpan.is_segment).toBe(false); + expect(childSpan.attributes[USER_AGENT_ORIGINAL]).toEqual({ type: 'string', value: expect.any(String) }); + // The document URL and referer only belong on the segment span. + expect(childSpan.attributes[URL_FULL]).toBeUndefined(); + expect(childSpan.attributes['http.request.header.referer']).toBeUndefined(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js index c51de9a85535..85b9d1a0fb2e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/existingIntegration/init.js @@ -8,7 +8,6 @@ window.Sentry = { }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js index dedd00bbcec5..d0ff6bbb92c9 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/feedbackIntegration/init.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js index 27950d9cf079..5991109fce66 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/moduleMetadataIntegration/init.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js index 98bcbc6c0d00..eb75d30bf760 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegration/init.js @@ -7,7 +7,6 @@ window.Sentry = { }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js index ba3766db49f5..d06b9b7b1dfd 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/lazyLoad/validIntegrationNpm/init.js @@ -6,7 +6,6 @@ import * as Sentry from '@sentry/browser'; window._testSentry = { ...Sentry }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js index b64706e66c9a..d5b05443ca9e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/browser'; import { moduleMetadataIntegration } from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [moduleMetadataIntegration()], beforeSend(event) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js index a72466f73a64..3c89709b3dfb 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js @@ -6,7 +6,6 @@ import { moduleMetadataIntegration } from '@sentry/browser'; import { rewriteFramesIntegration } from '@sentry/browser'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ moduleMetadataIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js index d7f5949f6033..631eb37b3eb1 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts index 5d066ae72ff1..1f3ece2217e5 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts @@ -1,12 +1,8 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { sentryTest } from '../../../../utils/fixtures'; -import { - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; async function mockSupabaseAuthRoutesSuccess(page: Page) { await page.route(/\/auth\/v1\/token\?grant_type=password/, route => { @@ -84,82 +80,96 @@ if (bundle.startsWith('bundle')) { sentryTest('should capture Supabase authentication spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseAuthRoutesSuccess(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const supabaseSpans = eventData.spans?.filter(({ op }) => op?.startsWith('db')); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const signInSpanPromise = waitForStreamedSpan(page, span => span.name === 'auth.signInWithPassword'); + const signOutSpanPromise = waitForStreamedSpan(page, span => span.name === 'auth.signOut'); + await page.goto(url); - expect(supabaseSpans).toHaveLength(2); - expect(supabaseSpans![0]).toMatchObject({ - description: 'auth signInWithPassword', - op: 'db', - parent_span_id: eventData.contexts?.trace?.span_id, + const [rootSpan, signInSpan, signOutSpan] = await Promise.all([ + rootSpanPromise, + signInSpanPromise, + signOutSpanPromise, + ]); + + expect(signInSpan).toMatchObject({ + name: 'auth.signInWithPassword', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.supabase', - 'db.operation.name': 'auth.signInWithPassword', - 'db.system.name': 'postgresql', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.supabase' }, + 'db.operation.name': { type: 'string', value: 'auth.signInWithPassword' }, + 'db.system.name': { type: 'string', value: 'postgresql' }, }), }); - expect(supabaseSpans![1]).toMatchObject({ - description: 'auth signOut', - op: 'db', - parent_span_id: eventData.contexts?.trace?.span_id, + expect(signOutSpan).toMatchObject({ + name: 'auth.signOut', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, status: 'ok', - data: expect.objectContaining({ - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.supabase', - 'db.operation.name': 'auth.signOut', - 'db.system.name': 'postgresql', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.supabase' }, + 'db.operation.name': { type: 'string', value: 'auth.signOut' }, + 'db.system.name': { type: 'string', value: 'postgresql' }, }), }); }); sentryTest('should capture Supabase authentication errors', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseAuthRoutesFailure(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const [errorEvent, transactionEvent] = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - - const supabaseSpans = transactionEvent.spans?.filter(({ op }) => op?.startsWith('db')); + const rootSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const signInSpanPromise = waitForStreamedSpan(page, span => span.name === 'auth.signInWithPassword'); + const errorRequestPromise = waitForErrorRequest( + page, + event => event.exception?.values?.[0]?.value === 'Invalid email or password', + ); + await page.goto(url); + + const [rootSpan, signInSpan, errorRequest] = await Promise.all([ + rootSpanPromise, + signInSpanPromise, + errorRequestPromise, + ]); + const errorEvent = envelopeRequestParser(errorRequest); expect(errorEvent.exception?.values?.[0].value).toBe('Invalid email or password'); - expect(supabaseSpans).toHaveLength(2); - expect(supabaseSpans![0]).toMatchObject({ - description: 'auth signInWithPassword', - op: 'db', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + expect(signInSpan).toMatchObject({ + name: 'auth.signInWithPassword', + parent_span_id: rootSpan.span_id, span_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - status: 'internal_error', - data: expect.objectContaining({ - 'sentry.op': 'db', - 'sentry.origin': 'auto.db.supabase', - 'db.operation.name': 'auth.signInWithPassword', - 'db.system.name': 'postgresql', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + status: 'error', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'db' }, + 'sentry.origin': { type: 'string', value: 'auto.db.supabase' }, + 'db.operation.name': { type: 'string', value: 'auth.signInWithPassword' }, + 'db.system.name': { type: 'string', value: 'postgresql' }, }), }); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js index 1e93dfea4606..0f600426009f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; const supabaseClient = createClient('https://test.supabase.co', 'test-key'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), Sentry.supabaseIntegration({ supabaseClient })], tracesSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts index 132a473e35f1..c392118f3e3d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/supabase/db-operations/test.ts @@ -1,12 +1,7 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; async function mockSupabaseRoute(page: Page) { await page.route('**/rest/v1/todos**', route => { @@ -30,14 +25,16 @@ if (bundle.startsWith('bundle')) { sentryTest('should capture Supabase database operation breadcrumbs', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventPromise = waitForErrorRequest(page, event => event.exception?.values?.[0]?.value === 'Test Error'); + await page.goto(url); + const eventData = envelopeRequestParser(await eventPromise); expect(eventData.breadcrumbs).toBeDefined(); expect(eventData.breadcrumbs).toContainEqual({ @@ -53,34 +50,32 @@ sentryTest('should capture Supabase database operation breadcrumbs', async ({ ge sentryTest('should capture multiple Supabase operations in sequence', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); - - expect(events).toHaveLength(2); - - events.forEach(event => { - expect( - event.breadcrumbs?.some(breadcrumb => breadcrumb.type === 'supabase' && breadcrumb?.category?.startsWith('db.')), - ).toBe(true); - }); + const eventPromise = waitForErrorRequest(page, event => event.exception?.values?.[0]?.value === 'Test Error'); + await page.goto(url); + const event = envelopeRequestParser(await eventPromise); + const supabaseBreadcrumbs = event.breadcrumbs?.filter(breadcrumb => breadcrumb.type === 'supabase'); + expect(supabaseBreadcrumbs?.map(breadcrumb => breadcrumb.category)).toEqual(['db.insert', 'db.select']); }); sentryTest('should include correct data payload in Supabase breadcrumbs', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { - return; + sentryTest.skip(); } await mockSupabaseRoute(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventPromise = waitForErrorRequest(page, event => event.exception?.values?.[0]?.value === 'Test Error'); + await page.goto(url); + const eventData = envelopeRequestParser(await eventPromise); const supabaseBreadcrumb = eventData.breadcrumbs?.find(b => b.type === 'supabase'); diff --git a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js index c5b7ded472bd..9b8269790d41 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/thirdPartyErrorsFilter/init.js @@ -25,7 +25,6 @@ _sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.as ); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ thirdPartyErrorFilterIntegration({ behaviour: 'apply-tag-if-contains-third-party-frames', filterKeys: ['my-app'] }), diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js index 069ac69900b5..16e92edb9230 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js @@ -4,7 +4,6 @@ import { viewHierarchyIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [viewHierarchyIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js b/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js index 2b6db24fef69..100b16a2d408 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/browser'; // Initialize Sentry with webWorker integration Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/ipv6/init.js b/dev-packages/browser-integration-tests/suites/ipv6/init.js index 2016ec4e2498..de8412c65a86 100644 --- a/dev-packages/browser-integration-tests/suites/ipv6/init.js +++ b/dev-packages/browser-integration-tests/suites/ipv6/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@[2001:db8::1]/1337', sendClientReports: false, defaultIntegrations: false, diff --git a/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js index 640d3dc01e8c..5d5cb2799a4e 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/force-init-chrome-extension/init.js @@ -6,7 +6,6 @@ window.Sentry = Sentry; window.chrome = { runtime: { id: 'mock-extension-id' } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', skipBrowserExtensionCheck: true, }); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js index c49c01424c29..7494359a0a8a 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js @@ -6,6 +6,5 @@ window.Sentry = Sentry; window.browser = { runtime: { id: 'mock-extension-id' } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js index 3c7486e35a1d..6cb3b49ceb53 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js @@ -6,6 +6,5 @@ window.Sentry = Sentry; window.chrome = { runtime: { id: 'mock-extension-id' } }; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js index 46a9f9358fc7..c21a085e735e 100644 --- a/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/acs/getCurrentScope/init.js @@ -15,6 +15,5 @@ window.__SENTRY__ = { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js index 14b0455ede2e..0bb1961ce1d9 100644 --- a/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js +++ b/dev-packages/browser-integration-tests/suites/old-sdk-interop/hub/isOlderThan/init.js @@ -15,6 +15,5 @@ window.__SENTRY__ = { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js b/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/addBreadcrumb/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js index a649c5b5e29a..3796a084234a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/errorEvent/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', defaultIntegrations: false, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js index 2fd49d572deb..099425cd3f2f 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureException/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', beforeSend(event) { Sentry.captureFeedback({ diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js index e52afed53f2b..b1cc371e5ec6 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureFeedback/withCaptureMessage/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', beforeSend(event) { Sentry.captureFeedback({ diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js index fb8609f66043..99ffd9f0ab31 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/multipleMessageAttachStacktrace/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', attachStacktrace: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js index fb8609f66043..99ffd9f0ab31 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', attachStacktrace: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js index bd0421b32761..7c200c542c56 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js index 0a4b1d7a5f94..baedf4f10a29 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/overrideIp/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, dataCollection: { userInfo: false }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts index 7d048c9e9b48..9d3209772cf5 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/performance/test.ts @@ -1,18 +1,17 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; -sentryTest('sets user.ip_address to "auto" on transactions by default', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('infers user IP and user agent on streamed spans by default', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const transaction = envelopeRequestParser(req); - expect(transaction.sdk?.settings?.infer_ip).toBe('auto'); + const envelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && span.name === 'woot'), + ); + await page.goto(url); + + const envelope = await envelopePromise; + expect(envelope[1][0][1].ingest_settings).toEqual({ infer_ip: 'auto', infer_user_agent: 'auto' }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js index 687dd524d917..530fceb8ec7b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/replay/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js index 596d13f7af7b..b3b47b24b57e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/dataCollection/sessions/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/debug/init.js b/dev-packages/browser-integration-tests/suites/public-api/debug/init.js index b8a138c0fdbf..573e4fdcb621 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/debug/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/debug/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js b/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js index 78d16f24cc20..c16b31fd1c85 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/denyUrls/init.js @@ -5,7 +5,6 @@ window.Sentry = Sentry; window._errorCount = 0; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', denyUrls: ['foo.js'], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js index 052a5b96651e..9bb07ba0cc11 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts index c18d75464223..5faa0f4d6964 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/diagnoseSdkConnectivity/test.ts @@ -1,6 +1,14 @@ +import type { SerializedStreamedSpan, StreamedSpanEnvelope } from '@sentry/core'; import { expect } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '../../../utils/spanUtils'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { + envelopeRequestParser, + getEnvelopeType, + properFullEnvelopeParser, + shouldSkipTracingTest, + waitForErrorRequest, +} from '../../../utils/helpers'; sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ getLocalTestUrl, page }) => { const bundle = process.env.PW_BUNDLE; @@ -9,7 +17,16 @@ sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ ge sentryTest.skip(); } - const pageloadRequestPromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload'); + const spans: SerializedStreamedSpan[] = []; + page.on('request', request => { + if (!request.url().startsWith('https://dsn.ingest.sentry.io/')) { + return; + } + if (getEnvelopeType(request) === 'span') { + spans.push(...properFullEnvelopeParser(request)[1][0][1].items); + } + }); + const pageloadSpanPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); // mock sdk connectivity url to avoid making actual request to sentry.io page.route('**/api/4509632503087104/envelope/**/*', route => { @@ -30,17 +47,21 @@ sentryTest('makes a call to sentry.io to diagnose SDK connectivity', async ({ ge const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const pageLoadEvent = envelopeRequestParser(await pageloadRequestPromise); + await pageloadSpanPromise; // undefined is expected and means the request was successful expect(await diagnoseMessagePromise).toEqual('SDK connectivity: undefined'); // the request to sentry.io should not be traced, hence no http.client span should be sent. - const httpClientSpans = pageLoadEvent.spans?.filter(s => s.op === 'http.client'); + await page.evaluate(() => (window as any).Sentry.flush()); + const httpClientSpans = spans.filter(span => getSpanOp(span) === 'http.client'); expect(httpClientSpans).toHaveLength(0); // no fetch breadcrumb should be sent (only breadcrumb for the console log) - expect(pageLoadEvent.breadcrumbs).toEqual([ + const eventPromise = waitForErrorRequest(page, event => event.message === 'connectivity checked'); + await page.evaluate(() => (window as any).Sentry.captureMessage('connectivity checked')); + const event = envelopeRequestParser(await eventPromise); + expect(event.breadcrumbs).toEqual([ expect.objectContaining({ category: 'console', message: 'SDK connectivity: undefined', diff --git a/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js b/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js index b3e95f5cd7f9..e66214c5c0bf 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/ignoreErrors/init.js @@ -5,7 +5,6 @@ window.Sentry = Sentry; window._errorCount = 0; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', ignoreErrors: ['ignoreErrorTest'], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/init.js b/dev-packages/browser-integration-tests/suites/public-api/init/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/init/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/init/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js index cebc5f71b52b..2b5313b11a6d 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/init/stringSampleRate/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: '0', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js index 8e180dcfc1ee..15e8680cc075 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/eventListener/original-callback/init.js @@ -8,6 +8,5 @@ window.originalBuiltIns = { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onError/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/onUnhandledRejection/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/requestAnimationFrame/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js index 33923ae30df1..0223ee8568b4 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/setTimeoutFrozen/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', // We assert on the debug message, so we need to enable debug mode debug: true, diff --git a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/instrumentation/xhr/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js index 6e9fbee0b83c..7b5ee5690589 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/consoleLoggingIntegrationShim/init.js @@ -4,7 +4,6 @@ window.Sentry = Sentry; // consoleLoggingIntegration should not actually work, but still not error out Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, integrations: [Sentry.consoleLoggingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js index f8f1e800c1c7..5f00ab4126c4 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.consoleLoggingIntegration()], }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js index a0e95abc054b..c4b3013773c3 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/loggerShim/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js b/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js index f3c84a9f36f5..5590fbb90547 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/afterCaptureMetric/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js b/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js index 8a9f2f73c857..73c6e63ed335 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', diff --git a/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js b/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js index a2d04de8cfb6..e184d8a0116e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/metrics/metricsShim/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js b/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setContext/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js b/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setExtra/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js b/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setExtras/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js b/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setTag/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js b/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setTags/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js b/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/setUser/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js b/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/showReportDialog/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js b/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/withScope/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js b/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js index b1fa399a0611..2c19109f6fd9 100644 --- a/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/attachRawBodyFromRequest/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js b/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js index a31b02589f2b..7b73a029761d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/autoFlushOnFeedback/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js index a023909c786f..dfc142feaa09 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js index d83f950346bd..7024a6574324 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js b/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js index 172f476c24d7..f9dccbffb530 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/bufferStalledRequests/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js index 20035a47839b..7704e9934cbf 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js index 8c59786159ec..7bea93df6756 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js index 566e6650d4c5..d933111eb57e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js index 12e46e733956..f04edea57625 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js index ccab05b4662e..f8ffa20ceb8f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js index e8ca0b243c08..f71ddfdaaa9b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js @@ -9,7 +9,6 @@ window.Replay = replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js index 53f2e9191b09..0e1297ae8710 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js index 6e4e9a4e5cb5..ab1e26f4490a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js index 74d9998abeb2..973a0ae3efdc 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js b/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js index a919d5aed823..2cfec5213d3f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/customEvents/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/init.js b/dev-packages/browser-integration-tests/suites/replay/dsc/init.js index ed91c960bdec..6411031d12b5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), window.Replay], tracePropagationTargets: [/.*/], diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts index e63c45e42293..f3eec3a11fb2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -2,12 +2,8 @@ import { expect } from '@playwright/test'; import type * as Sentry from '@sentry/browser'; import type { EventEnvelopeHeaders } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForErrorRequest, - waitForTransactionRequest, -} from '../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForErrorRequest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../utils/spanUtils'; import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '../../../utils/replayHelpers'; type TestWindow = Window & { @@ -16,7 +12,7 @@ type TestWindow = Window & { }; sentryTest( - 'should add replay_id to dsc of transactions when in session mode', + 'should add replay_id to dsc of streamed spans when in session mode', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -26,7 +22,9 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); // Wait for this to be available await page.waitForFunction('!!window.Replay'); @@ -40,15 +38,9 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); expect(replay.session?.id).toBeDefined(); @@ -67,7 +59,7 @@ sentryTest( ); sentryTest( - 'should not add replay_id to dsc of transactions when in buffer mode', + 'should not add replay_id to dsc of streamed spans when in buffer mode', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -77,7 +69,9 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.evaluate(() => { (window as unknown as TestWindow).Replay.startBuffering(); @@ -88,15 +82,9 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); expect(replay.session?.id).toBeDefined(); @@ -114,7 +102,7 @@ sentryTest( ); sentryTest( - 'should add replay_id to dsc of transactions when switching from buffer to session mode', + 'should add replay_id to dsc of streamed spans when switching from buffer to session mode', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -124,7 +112,9 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.evaluate(() => { (window as unknown as TestWindow).Replay.startBuffering(); @@ -140,15 +130,9 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); expect(replay.session?.id).toBeDefined(); @@ -168,7 +152,7 @@ sentryTest( ); sentryTest( - 'should not add replay_id to dsc of transactions if replay is not enabled', + 'should not add replay_id to dsc of streamed spans if replay is not enabled', async ({ getLocalTestUrl, page, browserName }) => { // This is flaky on webkit, so skipping there... if (shouldSkipReplayTest() || shouldSkipTracingTest() || browserName === 'webkit') { @@ -178,20 +162,16 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); - const transactionReq = waitForTransactionRequest(page); + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ); await page.evaluate(async () => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123' }); - scope.addEventProcessor(event => { - event.transaction = 'testTransactionDSC'; - return event; - }); }); - const req0 = await transactionReq; - - const envHeader = envelopeRequestParser(req0, 0); + const [envHeader] = await spanEnvelopePromise; const replay = await getReplaySnapshot(page); @@ -222,16 +202,20 @@ sentryTest('should add replay_id to error DSC while replay is active', async ({ const error1Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #1'); const error2Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #2'); - // We want to wait for the transaction to be done, to ensure we have a consistent test - const transactionReq = hasTracing ? waitForTransactionRequest(page) : Promise.resolve(); + // We want to wait for the pageload span to end, to ensure we have a consistent test + const spanEnvelopePromise = hasTracing + ? waitForStreamedSpanEnvelope(page, envelope => + envelope[1][0][1].items.some(span => span.is_segment && getSpanOp(span) === 'pageload'), + ) + : Promise.resolve(); // Wait for this to be available await page.waitForFunction('!!window.Replay'); - // We have to start replay before we finish the transaction, otherwise the DSC will not be frozen with the Replay ID + // We have to start replay before we finish the pageload span, otherwise the DSC will not be frozen with the Replay ID await page.evaluate('window.Replay.start();'); await waitForReplayRunning(page); - await transactionReq; + await spanEnvelopePromise; await page.evaluate('window._triggerError(1)'); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js index da36e5671e30..83e514016c1b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js index b0e3b3b7f091..c24b8e1daca8 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js index 0a70bee557e1..5e71c58fc0aa 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorModeCustomTransport/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js index 29e87a8198af..7ab704b50d6a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js index e31b3b638884..a765fc48c67b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/init.js b/dev-packages/browser-integration-tests/suites/replay/errors/init.js index d83f950346bd..7024a6574324 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/errors/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js index 332aae5c8a0f..5a97a87c2571 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js index 0c788f53c321..82bc5a045870 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js index 332aae5c8a0f..5a97a87c2571 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js index 67b3d188f414..9db232f536cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js index 0c788f53c321..82bc5a045870 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js index 4696596df9f9..caca8e2d317e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, // We ensure to sample for errors, so by default nothing is sent diff --git a/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js b/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js index 837a81466609..c973a578baae 100644 --- a/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/fileInput/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/flushing/init.js b/dev-packages/browser-integration-tests/suites/replay/flushing/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/flushing/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/flushing/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js index e961e3ceb30c..269b531b57eb 100644 --- a/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/ignoreMutations/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/init.js b/dev-packages/browser-integration-tests/suites/replay/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js index 45d7fb7af6d3..e52033fd464e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index 69cc3b55cfb8..d09fe6530e9c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/logger/init.js b/dev-packages/browser-integration-tests/suites/replay/logger/init.js index f675e1af8681..195be16ddad3 100644 --- a/dev-packages/browser-integration-tests/suites/replay/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/logger/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js index 27092017f938..7bab07447b3a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js index 909da70f67a8..2a3dae73541d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js b/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js index 980127dd5ee9..58873b85f9ec 100644 --- a/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDurationLimit/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js index 4e5b196e47de..f5a01ece0a37 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js index 2e24918c3980..16000b4c4412 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/init.js @@ -12,7 +12,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js index c20873c35d32..96f5c063e13d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInput/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js index 5a234f35eecd..c20c8b36a4c2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -11,7 +11,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js index 5248651ac1c3..9200b5771ec6 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/init.js @@ -10,7 +10,6 @@ window.Replay = new Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js b/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js index a705f7431907..77fc89c70a25 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/requests/init.js b/dev-packages/browser-integration-tests/suites/replay/requests/init.js index d1e552c448b0..30fdb3198b62 100644 --- a/dev-packages/browser-integration-tests/suites/replay/requests/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/requests/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sampling/init.js b/dev-packages/browser-integration-tests/suites/replay/sampling/init.js index ce6e4b19eb22..6405faaf741b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sampling/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sampling/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js index 1ff49d804e31..ee55b5cca2c4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js index df437ea858a1..3af8409da672 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/init.js @@ -8,7 +8,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js index 2bf99b713783..214340ba12c5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js index 852014c9f109..b21921ae5726 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js index 8f7316b1d38f..77b6bf6b67e4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1, replaysSessionSampleRate: 0.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js b/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js index 708bb8533402..5083bd48c745 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js index 4acf8b57936f..6f2a54514433 100644 --- a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/init.js @@ -9,7 +9,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js index 7a2460d4059c..4d4476012c0d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js index 752b0a131101..cc0094a0366b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/init.js @@ -10,7 +10,6 @@ window.Replay = Sentry.replayIntegration({ }); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 0, replaysSessionSampleRate: 1.0, diff --git a/dev-packages/browser-integration-tests/suites/sessions/init.js b/dev-packages/browser-integration-tests/suites/sessions/init.js index 91040d71877d..af2df91a7ceb 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', }); diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js index 0ad971be30a2..2be1d55b5782 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js index 91040d71877d..af2df91a7ceb 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/page-lifecycle/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', }); diff --git a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js index 138a059a35a5..18ea8864bc65 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/route-lifecycle/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js b/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js index f5c6bdef5e5f..7bba2ce96cac 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/sampled-session-update/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', sampleRate: 0, diff --git a/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js b/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js index 138a059a35a5..18ea8864bc65 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/start-session/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/sessions/user/init.js b/dev-packages/browser-integration-tests/suites/sessions/user/init.js index 138a059a35a5..18ea8864bc65 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/user/init.js +++ b/dev-packages/browser-integration-tests/suites/sessions/user/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '0.1', integrations: [Sentry.browserSessionIntegration({ lifecycle: 'route' })], diff --git a/dev-packages/browser-integration-tests/suites/stacktraces/init.js b/dev-packages/browser-integration-tests/suites/stacktraces/init.js index d9c534b801cd..d8c94f36fdd0 100644 --- a/dev-packages/browser-integration-tests/suites/stacktraces/init.js +++ b/dev-packages/browser-integration-tests/suites/stacktraces/init.js @@ -3,6 +3,5 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', }); diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js index 226c60d5859b..d24b26bdffc7 100644 --- a/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js @@ -4,7 +4,6 @@ import { makeMultiplexedTransport } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: makeMultiplexedTransport(Sentry.makeFetchTransport, ({ getEvent }) => { const event = getEvent('event'); diff --git a/dev-packages/browser-integration-tests/suites/transport/offline/init.js b/dev-packages/browser-integration-tests/suites/transport/offline/init.js index 809d0c646ed1..e102d6a8a5a5 100644 --- a/dev-packages/browser-integration-tests/suites/transport/offline/init.js +++ b/dev-packages/browser-integration-tests/suites/transport/offline/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: Sentry.makeBrowserOfflineTransport(), }); diff --git a/dev-packages/browser-integration-tests/suites/wasm/init.js b/dev-packages/browser-integration-tests/suites/wasm/init.js index e4cffb1b682f..9b9aad7b911f 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/init.js @@ -4,7 +4,6 @@ import { wasmIntegration } from '@sentry/wasm'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [wasmIntegration()], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js b/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js index 602bdd6b9a16..2a0766dfa03f 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/instantiateBufferRegistration/init.js @@ -4,7 +4,6 @@ import { wasmIntegration } from '@sentry/wasm'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [wasmIntegration()], beforeSend: event => { diff --git a/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js b/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js index 85c1fde92c15..912a4aafd728 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/thirdPartyFilter/init.js @@ -25,7 +25,6 @@ _sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.as window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ wasmIntegration({ applicationKey: 'wasm-test-app' }), diff --git a/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js b/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js index e02171c7ae8c..39007ede4bc0 100644 --- a/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js +++ b/dev-packages/browser-integration-tests/suites/wasm/webWorker/init.js @@ -4,7 +4,6 @@ import { wasmIntegration } from '@sentry/wasm'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [wasmIntegration({ applicationKey: 'wasm-worker-app' })], }); From 212fd130d629ff54875c485600f1122154407065 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:48:11 +0200 Subject: [PATCH 33/73] chore(bugbot): Flag hard-coded attribute names that have a conventions constant (#25070) ## What Bugbot now flags attribute names and span ops written as strings (e.g. `'sentry.origin'`) when `@sentry/conventions` has a constant for them. The rule applies to SDK source and to tests. ## Why Reviewers often have to point this out by hand, for example in https://github.com/getsentry/sentry-javascript/pull/25027#discussion_r4182239871. Co-authored-by: Claude Opus 5.5 --- .cursor/BUGBOT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 2a17708c2752..f70b4bc51da0 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -76,6 +76,7 @@ Keep reviews high-signal. Prefer actionable, high-confidence findings over specu - Flag usage of the following APIs: `getCurrentScope()`, `getIsolationScope()`, `getClient()` if they are avoidable. Flag it with severity Low and acknowledge from the start that this is more a "is this necessary" check, rather than a rule violation. - Reason for flagging: Usage of these APIs is problematic for multi-client setups where either there is no "current" client/scope, or the wrong client might be used. Calling these APIs would create a current scope, thereby misleading any future calls to these APIs. - What to do instead: Use an existing reference to the scope or client. For example, this is possible in most `Integration` hooks. +- Flag hard-coded attribute names and span ops (e.g. `'sentry.origin'`, `'sentry.op'`, `'http.request.method'`, `'db.system.name'`) when `@sentry/conventions` exports a constant for them. Use the constant from `@sentry/conventions/attributes` (e.g. `SENTRY_ORIGIN`, `SENTRY_OP`) or `@sentry/conventions/op` instead. Do not flag values that have no constant in `@sentry/conventions`. - Flag unnecessary `span.setAttribute(s)` calls: If data is already available at span start, it must be set via the `attributes` option of `startSpan`, `startSpanManual`, `startInactiveSpan` or `startIdleSpan` calls. This ensures that as much context as possible is available when `tracesSampler` or `ignoreSpans` SDK options are applied. If a `span.setAttribute(s)` call happens at a later time than right after span start and the attribute value can only be computed at that time, do not flag it. ### Code quality @@ -92,6 +93,7 @@ Keep reviews high-signal. Prefer actionable, high-confidence findings over specu - Flag usage of `expect.objectContaining` and other relaxed assertions, when a test expects something NOT to be included in a payload but there's no respective assertion. - Flag usage of conditionals in one test and recommend splitting up the test for the different paths. - Flag usage of loops testing multiple scenarios in one test and recommend using `(it)|(test).each` instead. +- Apply the `@sentry/conventions` constants rule from above to test files too. Test assertions on span attributes and ops must use the constants, not string literals. - Flag tests that are likely to introduce flakes. In our case this usually means we wait for some telemetry requests sent from an SDK. Patterns to look out for: - Only waiting for a request, after an action is performed. Instead, start waiting, perform action, await request promise. - Race conditions when waiting on multiple requests. Ensure that waiting checks are unique enough and don't depend on a hard order when there's a chance that telemetry can be sent in arbitrary order. From 3dae6a50f7dd45d1dccf79b14f468148095e62d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 6 Oct 2026 10:55:42 +0300 Subject: [PATCH 34/73] fix(server-utils): Map Flue messages and token usage to the gen_ai conventions (#24992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flue records the messages of its turns in pi-ai's own shape, with a `toolResult` role and `toolCall` parts. Sentry's AI views only render the gen_ai conventions shape, so tool calls and tool results were missing from Flue conversations, and the tool result showed up with an invalid role. This maps the input and output messages to the conventions, the way #24352 did for Google GenAI. The token counts come from pi-ai too. Its `input` leaves the cached tokens out, while `gen_ai.usage.input_tokens` includes them by definition, so input tokens and input cost were too low whenever prompt caching was active (the default for Anthropic models). The finish reason is mapped as well (`toolUse` becomes `tool_call`), and the tool span now records the result the model receives (Flue's `effectiveResult`) instead of the internal `{ content, details }` wrapper. The mapper lives in `ai/pi-ai/` because pi-durable sends the same shapes, and its integration uses it too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../tracing/flue/instrument-with-pii.mjs | 10 + .../suites/tracing/flue/test.ts | 66 +++++- packages/server-utils/src/ai/flue/index.ts | 38 +--- packages/server-utils/src/ai/flue/types.ts | 21 +- packages/server-utils/src/ai/flue/utils.ts | 105 +++++----- .../server-utils/src/ai/pi-ai/messages.ts | 104 ++++++++++ .../server-utils/src/ai/pi-ai/providers.ts | 27 +++ packages/server-utils/src/ai/pi-ai/usage.ts | 75 +++++++ packages/server-utils/src/ai/types.ts | 10 + .../test/ai/lib/tracing/flue.test.ts | 188 +++++++++++++++++- .../test/ai/lib/utils/pi-ai-messages.test.ts | 150 ++++++++++++++ .../test/ai/lib/utils/pi-ai-usage.test.ts | 62 ++++++ 12 files changed, 754 insertions(+), 102 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs create mode 100644 packages/server-utils/src/ai/pi-ai/messages.ts create mode 100644 packages/server-utils/src/ai/pi-ai/providers.ts create mode 100644 packages/server-utils/src/ai/pi-ai/usage.ts create mode 100644 packages/server-utils/src/ai/types.ts create mode 100644 packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts create mode 100644 packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs new file mode 100644 index 000000000000..1c507ba84b9c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/flue/instrument-with-pii.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + dataCollection: { genAI: { inputs: true, outputs: true } }, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts index 98ad59dcc4df..f4cf35579ae4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/flue/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/flue/test.ts @@ -2,8 +2,13 @@ import { GEN_AI_AGENT_NAME, GEN_AI_CONVERSATION_ID, GEN_AI_COST_TOTAL_TOKENS, + GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, + GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, + GEN_AI_SYSTEM_INSTRUCTIONS, + GEN_AI_TOOL_CALL_ARGUMENTS, + GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_NAME, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, @@ -87,9 +92,10 @@ conditionalTest({ min: 22 })('Flue integration', () => { expect(chat.attributes[GEN_AI_COST_TOTAL_TOKENS]?.value).toEqual(expect.any(Number)); } + // pi-ai's `toolUse` is reported as the conventions' `tool_call`. expect(chats.map(chat => chat.attributes[GEN_AI_RESPONSE_FINISH_REASONS]?.value).sort()).toEqual([ '["stop"]', - '["toolUse"]', + '["tool_call"]', ]); const tool = tools[0]!; @@ -111,4 +117,62 @@ conditionalTest({ min: 22 })('Flue integration', () => { }, FLUE_DEPENDENCIES, ); + + createEsmAndCjsTests( + __dirname, + 'scenario.mjs', + 'instrument-with-pii.mjs', + (createRunner, test, mode) => { + if (mode === 'cjs') { + return; + } + + test('records the messages of the turns in the gen_ai conventions shape', async () => { + await createRunner() + .expect({ + span: container => { + const chats = container.items + .filter(span => span.name === 'chat faux-model') + .sort((a, b) => a.start_timestamp - b.start_timestamp); + expect(chats).toHaveLength(2); + + // Flue sends pi-ai's shape (`toolResult`, `toolCall`); Sentry renders the conventions. + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ + { role: 'user', parts: [{ type: 'text', content: 'What is the weather in Berlin?' }] }, + ]); + expect(JSON.parse(String(chats[0]!.attributes[GEN_AI_OUTPUT_MESSAGES]?.value))).toEqual([ + { + role: 'assistant', + parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Berlin"}' }], + finish_reason: 'tool_call', + }, + ]); + + const answerInput = JSON.parse(String(chats[1]!.attributes[GEN_AI_INPUT_MESSAGES]?.value)); + expect(answerInput.map((message: { role: string }) => message.role)).toEqual([ + 'user', + 'assistant', + 'tool', + ]); + expect(answerInput[2].parts[0]).toMatchObject({ + type: 'tool_call_response', + id: 'call_1', + name: 'get_weather', + }); + expect(answerInput[2].parts[0].result).toContain('sunny in Berlin'); + expect(chats[1]!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]?.value).toContain('You are a helpful assistant.'); + + // The tool span records the result the model receives, not Flue's internal wrapper. + const tool = container.items.find(span => span.name === 'execute_tool get_weather')!; + expect(tool.attributes[GEN_AI_TOOL_CALL_ARGUMENTS]?.value).toBe('{"city":"Berlin"}'); + expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).toContain('sunny in Berlin'); + expect(tool.attributes[GEN_AI_TOOL_CALL_RESULT]?.value).not.toContain('details'); + }, + }) + .start() + .completed(); + }); + }, + FLUE_DEPENDENCIES, + ); }); diff --git a/packages/server-utils/src/ai/flue/index.ts b/packages/server-utils/src/ai/flue/index.ts index 7243aeb1351f..44c9ade4667c 100644 --- a/packages/server-utils/src/ai/flue/index.ts +++ b/packages/server-utils/src/ai/flue/index.ts @@ -1,24 +1,14 @@ import type { Span } from '@sentry/core'; -import { - _INTERNAL_shouldSkipAiProviderWrapping, - _INTERNAL_skipAiProviderWrapping, - continueTrace, - getActiveSpan, - LRUMap, - startSpan, - withActiveSpan, -} from '@sentry/core'; +import { continueTrace, getActiveSpan, LRUMap, startSpan, withActiveSpan } from '@sentry/core'; import { GEN_AI_AGENT_NAME, GEN_AI_CONVERSATION_ID, GEN_AI_OPERATION_NAME, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants'; import type { GenAiOptions } from '../core/utils'; import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; -import { GOOGLE_GENAI_INTEGRATION_NAME } from '../google-genai/constants'; -import { OPENAI_INTEGRATION_NAME } from '../openai/constants'; +import { skipPiAiProviderIntegrations } from '../pi-ai/providers'; import { FLUE_INSTRUMENTATION_KEY, FLUE_OPERATION, FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants'; import type { SpanTracker } from './utils'; import { @@ -33,8 +23,6 @@ import type { FlueInstrumentation } from './types'; export type FlueOptions = GenAiOptions; -const SKIPPED_PROVIDERS = [OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAME, GOOGLE_GENAI_INTEGRATION_NAME]; - /** * Build the object to hand to `instrument()` from `@flue/runtime`. * @@ -49,20 +37,16 @@ const SKIPPED_PROVIDERS = [OPENAI_INTEGRATION_NAME, ANTHROPIC_AI_INTEGRATION_NAM * which fall back to the current client's `dataCollection.genAI` settings and are read per event. */ export function createFlueInstrumentation(options: FlueOptions = {}): FlueInstrumentation { - // Flue drives the providers through `@earendil-works/pi-ai`, which bundles the `openai`, - // `@anthropic-ai/sdk` and `@google/genai` clients. Left alone they instrument the same call this - // reports as a turn, emitting a second `gen_ai.chat` beside ours. + // Flue drives the providers through `@earendil-works/pi-ai`, which sends its requests through the + // provider SDKs that Sentry instruments too (see `skipPiAiProviderIntegrations`). Left alone, their + // integrations report the same request a second time, as a second `gen_ai.chat` beside the turn's. // - // Applied on first use rather than here, for two reasons. Constructing the object proves nothing - // — if `instrument()` rejects it, suppressing the provider integrations would leave the app with - // no `gen_ai.chat` spans at all. And the registry is reset per client (`_setupIntegrations` - // clears it, and Cloudflare calls `init()` per request), so a one-shot call at module scope is - // wiped by the next `init()` and every later request double-reports. - const skipProviders = (): void => { - if (!SKIPPED_PROVIDERS.every(provider => _INTERNAL_shouldSkipAiProviderWrapping(provider))) { - _INTERNAL_skipAiProviderWrapping(SKIPPED_PROVIDERS); - } - }; + // The provider integrations are skipped on first use rather than here, for two reasons. + // Constructing the object proves nothing: if `instrument()` rejects it, suppressing the provider + // integrations would leave the app with no `gen_ai.chat` spans at all. And the registry is reset per + // client (`_setupIntegrations` clears it, and Cloudflare calls `init()` per request), so a one-shot + // call at module scope is wiped by the next `init()` and every later request double-reports. + const skipProviders = skipPiAiProviderIntegrations; // Keyed by the agent operation's own id, which is what the observations carry. That keeps // concurrent runs apart and gives a delegated subagent its own span: Flue nests a second `agent` diff --git a/packages/server-utils/src/ai/flue/types.ts b/packages/server-utils/src/ai/flue/types.ts index bdbd58455518..30850dea9949 100644 --- a/packages/server-utils/src/ai/flue/types.ts +++ b/packages/server-utils/src/ai/flue/types.ts @@ -6,21 +6,7 @@ * `FlueExecutionContext` as of `@flue/runtime` 2.x. */ -/** Token counts and Flue-computed costs on a settled turn. */ -export interface FlueUsage { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - totalTokens?: number; - cost?: { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; - total?: number; - }; -} +import type { PiAiUsage } from '../pi-ai/usage'; /** Mirrors `ModelRequestInfo`. */ export interface FlueModelRequestInfo { @@ -51,7 +37,8 @@ export interface FlueModelResponse { responseId?: string; responseModel?: string; output?: unknown; - usage?: FlueUsage; + /** pi-ai's usage, which Flue passes through with its own cost figures. */ + usage?: PiAiUsage; finishReason?: string; } @@ -88,6 +75,8 @@ export interface FlueObservation { request?: FlueModelRequest; args?: unknown; result?: unknown; + /** The tool result as the model sees it, on successful `tool` events. */ + effectiveResult?: unknown; response?: FlueModelResponse; } diff --git a/packages/server-utils/src/ai/flue/utils.ts b/packages/server-utils/src/ai/flue/utils.ts index 2661eea0892c..7f22256bf319 100644 --- a/packages/server-utils/src/ai/flue/utils.ts +++ b/packages/server-utils/src/ai/flue/utils.ts @@ -1,12 +1,14 @@ import type { LRUMap, Span } from '@sentry/core'; -import { captureException, SPAN_STATUS_ERROR, startInactiveSpan, stringify, withActiveSpan } from '@sentry/core'; +import { + captureException, + isObjectLike, + SPAN_STATUS_ERROR, + startInactiveSpan, + stringify, + withActiveSpan, +} from '@sentry/core'; import { GEN_AI_CONVERSATION_ID, - GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, - GEN_AI_COST_CACHE_READ_INPUT_TOKENS, - GEN_AI_COST_INPUT_TOKENS, - GEN_AI_COST_OUTPUT_TOKENS, - GEN_AI_COST_TOTAL_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_OUTPUT_MESSAGES, @@ -23,18 +25,20 @@ import { GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_DEFINITIONS, GEN_AI_TOOL_NAME, - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, - GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, SERVER_ADDRESS, SERVER_PORT, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { getGenAiSpanOp } from '../core/utils'; +import { + piAiAssistantMessageToGenAiMessage, + piAiContentToString, + piAiFinishReason, + piAiMessagesToGenAiMessages, +} from '../pi-ai/messages'; +import { setPiAiUsageAttributes } from '../pi-ai/usage'; import { FLUE_ORIGIN, MAX_TRACKED_FLUE_SPANS } from './constants'; -import type { FlueErrorInfo, FlueModelRequestInfo, FlueObservation, FlueUsage } from './types'; +import type { FlueErrorInfo, FlueModelRequestInfo, FlueObservation } from './types'; /** * Flue persists the incoming W3C `traceparent` at admission and replays it on the agent operation. @@ -157,22 +161,25 @@ export function endTurnSpan(observation: FlueObservation, turnSpans: SpanTracker setRequestAttributes(span, observation.request); - const { responseId, finishReason } = observation.response ?? {}; + const responseId = observation.response?.responseId; if (responseId) { span.setAttribute(GEN_AI_RESPONSE_ID, responseId); } + const finishReason = piAiFinishReason(observation.response?.finishReason); if (finishReason) { // Serialized, not a raw array: the conventions declare this attribute's value type as `string`, // and that is what `ai/core`, Mastra, OpenAI and Vercel AI all write. span.setAttribute(GEN_AI_RESPONSE_FINISH_REASONS, stringify([finishReason])); } - const output = observation.response?.output; - if (recordOutputs && output !== undefined) { - span.setAttribute(GEN_AI_OUTPUT_MESSAGES, stringify(output)); + const output = recordOutputs + ? piAiAssistantMessageToGenAiMessage(observation.response?.output, finishReason) + : undefined; + if (output) { + span.setAttribute(GEN_AI_OUTPUT_MESSAGES, stringify([output])); } - setUsageAttributes(span, observation.response?.usage, observation.isError); + setPiAiUsageAttributes(span, observation.response?.usage, observation.isError); if (observation.isError) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); @@ -180,39 +187,6 @@ export function endTurnSpan(observation: FlueObservation, turnSpans: SpanTracker span.end(); } -/** - * Flue reports token counts and its own computed costs on the same `usage` object, so both are set - * here. The cost figures have no equivalent in the provider SDKs' own instrumentation. - */ -export function setUsageAttributes(span: Span, usage: FlueUsage | undefined, isError?: boolean): void { - // A turn that failed before the provider billed anything reports every counter as 0. Writing - // those is noise that reads as a real zero-cost call, so skip the block entirely. - if (!usage || (isError && !usage.totalTokens)) { - return; - } - - const attributes: Record = {}; - const set = (key: string, value: number | undefined): void => { - if (typeof value === 'number') { - attributes[key] = value; - } - }; - - set(GEN_AI_USAGE_INPUT_TOKENS, usage.input); - set(GEN_AI_USAGE_OUTPUT_TOKENS, usage.output); - set(GEN_AI_USAGE_TOTAL_TOKENS, usage.totalTokens); - set(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, usage.cacheRead); - set(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, usage.cacheWrite); - - set(GEN_AI_COST_INPUT_TOKENS, usage.cost?.input); - set(GEN_AI_COST_OUTPUT_TOKENS, usage.cost?.output); - set(GEN_AI_COST_TOTAL_TOKENS, usage.cost?.total); - set(GEN_AI_COST_CACHE_READ_INPUT_TOKENS, usage.cost?.cacheRead); - set(GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, usage.cost?.cacheWrite); - - span.setAttributes(attributes); -} - /** * Tool spans hang off the agent invocation rather than the turn, matching how Flue's own * OpenTelemetry adapter projects them: siblings of `chat`, correlated to model output by tool call @@ -251,8 +225,11 @@ export function endToolSpan(observation: FlueObservation, toolSpans: SpanTracker } toolSpans.remove(toolCallId); - if (recordOutputs && observation.result !== undefined) { - span.setAttribute(GEN_AI_TOOL_CALL_RESULT, stringify(observation.result)); + if (recordOutputs) { + const result = toolResultForModel(observation); + if (result !== undefined) { + span.setAttribute(GEN_AI_TOOL_CALL_RESULT, result); + } } if (observation.isError) { @@ -262,6 +239,25 @@ export function endToolSpan(observation: FlueObservation, toolSpans: SpanTracker span.end(); } +/** + * The result the model receives: Flue's `effectiveResult` when it has one, else the content blocks + * of its harness-level `result` (`{ content, details }`, whose `details` payload is tool-specific). + */ +function toolResultForModel(observation: FlueObservation): string | undefined { + const { effectiveResult } = observation; + if (effectiveResult !== undefined) { + if (typeof effectiveResult === 'string') { + return effectiveResult; + } + return Array.isArray(effectiveResult) ? piAiContentToString(effectiveResult) : stringify(effectiveResult); + } + const { result } = observation; + if (isObjectLike(result) && Array.isArray(result.content)) { + return piAiContentToString(result.content); + } + return result === undefined ? undefined : stringify(result); +} + /** * `turn_request` is the only event carrying the request's content — the settled `turn` reports * metadata alone — so input messages, system prompt and tool definitions are read from it. @@ -277,8 +273,9 @@ export function recordRequestContent(observation: FlueObservation, turnSpans: Sp if (input.systemPrompt) { span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, input.systemPrompt); } - if (input.messages) { - span.setAttribute(GEN_AI_INPUT_MESSAGES, stringify(input.messages)); + const messages = piAiMessagesToGenAiMessages(input.messages); + if (messages.length) { + span.setAttribute(GEN_AI_INPUT_MESSAGES, stringify(messages)); } if (input.tools?.length) { span.setAttribute(GEN_AI_TOOL_DEFINITIONS, stringify(input.tools)); diff --git a/packages/server-utils/src/ai/pi-ai/messages.ts b/packages/server-utils/src/ai/pi-ai/messages.ts new file mode 100644 index 000000000000..c5060cdb1181 --- /dev/null +++ b/packages/server-utils/src/ai/pi-ai/messages.ts @@ -0,0 +1,104 @@ +import { isObjectLike, stringify } from '@sentry/core'; +import type { GenAiMessage, GenAiMessagePart } from '../types'; + +/** + * Map pi-ai messages (`@earendil-works/pi-ai`, which Flue and pi-durable send) to the gen_ai + * conventions. Sentry renders neither pi-ai's `toolResult` role nor its `toolCall` parts. + * + * System messages are left out: their prompt belongs in `gen_ai.system_instructions`. Messages with + * a role the conventions do not have are left out too. + */ +export function piAiMessagesToGenAiMessages(messages: unknown): GenAiMessage[] { + if (!Array.isArray(messages)) { + return []; + } + + const mapped: GenAiMessage[] = []; + for (const message of messages) { + if ( + !isObjectLike(message) || + (message.role !== 'user' && message.role !== 'assistant' && message.role !== 'toolResult') + ) { + continue; + } + + if (message.role === 'toolResult') { + mapped.push({ + role: 'tool', + parts: [ + { + type: 'tool_call_response', + id: message.toolCallId, + name: message.toolName, + result: piAiContentToString(message.content), + }, + ], + }); + continue; + } + + const parts = piAiContentToParts(message.content); + if (parts.length) { + mapped.push({ role: message.role, parts }); + } + } + return mapped; +} + +/** The `gen_ai.output.messages` entry of a pi-ai assistant message, or `undefined` when it has no content. */ +export function piAiAssistantMessageToGenAiMessage(message: unknown, finishReason?: string): GenAiMessage | undefined { + const parts = isObjectLike(message) ? piAiContentToParts(message.content) : []; + if (!parts.length) { + return undefined; + } + return { role: 'assistant', parts, ...(finishReason ? { finish_reason: finishReason } : {}) }; +} + +/** pi-ai names a tool-calling stop `toolUse`; the conventions call it `tool_call`. */ +export function piAiFinishReason(stopReason: unknown): string | undefined { + if (typeof stopReason !== 'string' || !stopReason) { + return undefined; + } + return stopReason === 'toolUse' ? 'tool_call' : stopReason; +} + +/** Text-only content as its text, joined the way pi-ai joins it; anything else as its mapped parts. */ +export function piAiContentToString(content: unknown): string | undefined { + const parts = piAiContentToParts(content); + return parts.every(part => part.type === 'text') ? parts.map(part => part.content).join('\n') : stringify(parts); +} + +function piAiContentToParts(content: unknown): GenAiMessagePart[] { + if (typeof content === 'string') { + return content ? [{ type: 'text', content }] : []; + } + if (!Array.isArray(content)) { + return []; + } + return content.map(piAiPartToGenAiPart).filter((part): part is GenAiMessagePart => part !== undefined); +} + +/** An image is reported by its media type only: its `data` is base64, which the conventions require to be dropped. */ +function piAiPartToGenAiPart(part: unknown): GenAiMessagePart | undefined { + if (!isObjectLike(part)) { + return undefined; + } + + switch (part.type) { + case 'text': + // An empty text block makes Sentry render the message as "(no value)" instead of its tool calls. + return typeof part.text === 'string' && part.text.trim() ? { type: 'text', content: part.text } : undefined; + case 'thinking': + // A redacted thinking block carries an encrypted payload, not text. + return typeof part.thinking === 'string' && part.thinking && !part.redacted + ? { type: 'reasoning', content: part.thinking } + : undefined; + case 'toolCall': + return { type: 'tool_call', id: part.id, name: part.name, arguments: stringify(part.arguments ?? {}, String) }; + case 'image': + return { type: 'blob', mime_type: part.mimeType }; + default: + // Part kinds we don't know yet render as JSON rather than disappearing. + return { type: 'object', content: part }; + } +} diff --git a/packages/server-utils/src/ai/pi-ai/providers.ts b/packages/server-utils/src/ai/pi-ai/providers.ts new file mode 100644 index 000000000000..524afe79d573 --- /dev/null +++ b/packages/server-utils/src/ai/pi-ai/providers.ts @@ -0,0 +1,27 @@ +import { _INTERNAL_shouldSkipAiProviderWrapping, _INTERNAL_skipAiProviderWrapping } from '@sentry/core'; +import { ANTHROPIC_AI_INTEGRATION_NAME } from '../anthropic-ai/constants'; +import { GOOGLE_GENAI_INTEGRATION_NAME } from '../google-genai/constants'; +import { OPENAI_INTEGRATION_NAME } from '../openai/constants'; + +// pi-ai sends its requests through the `openai`, `@anthropic-ai/sdk` and `@google/genai` clients. +// Left alone, those integrations report the same request a second time beside the `chat` span of +// the framework that sent it. Bedrock requests go through `@aws-sdk/client-bedrock-runtime`, which +// `awsIntegration` still reports; that one has no skip yet. +const PI_AI_PROVIDER_INTEGRATIONS = [ + OPENAI_INTEGRATION_NAME, + ANTHROPIC_AI_INTEGRATION_NAME, + GOOGLE_GENAI_INTEGRATION_NAME, +]; + +/** + * Stop the provider SDK integrations from reporting the requests pi-ai sends. The skip is + * process-wide: the provider SDKs do not know which of their calls pi-ai made. + * + * The registry is reset per client, so callers apply this on the first request instead of once at + * setup, or the next `init()` would undo it. + */ +export function skipPiAiProviderIntegrations(): void { + if (!PI_AI_PROVIDER_INTEGRATIONS.every(provider => _INTERNAL_shouldSkipAiProviderWrapping(provider))) { + _INTERNAL_skipAiProviderWrapping(PI_AI_PROVIDER_INTEGRATIONS); + } +} diff --git a/packages/server-utils/src/ai/pi-ai/usage.ts b/packages/server-utils/src/ai/pi-ai/usage.ts new file mode 100644 index 000000000000..4bfb8869e72e --- /dev/null +++ b/packages/server-utils/src/ai/pi-ai/usage.ts @@ -0,0 +1,75 @@ +import type { Span } from '@sentry/core'; +import { + GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_COST_CACHE_READ_INPUT_TOKENS, + GEN_AI_COST_INPUT_TOKENS, + GEN_AI_COST_OUTPUT_TOKENS, + GEN_AI_COST_TOTAL_TOKENS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; + +/** + * Token counts and costs of a pi-ai response. `input` and `cost.input` leave the cache tokens out; + * `reasoning` is part of `output`. + */ +export interface PiAiUsage { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + reasoning?: number; + totalTokens?: number; + cost?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + total?: number; + }; +} + +/** + * Set the token and cost attributes of a pi-ai response. The conventions count cached tokens in + * `gen_ai.usage.input_tokens` and their cost in `gen_ai.cost.input_tokens`, so both are summed here. + * + * `unbilled` is true for a response the provider may not have billed: a failed request, or one the + * provider parked to answer later. Its counters are all 0, which would read as a real zero-cost + * call, so nothing is set for it unless it reports tokens. + */ +export function setPiAiUsageAttributes(span: Span, usage: PiAiUsage | undefined, unbilled?: boolean): void { + if (!usage || (unbilled && !usage.totalTokens)) { + return; + } + + const attributes: Record = {}; + const set = (key: string, value: number | undefined): void => { + if (typeof value === 'number') { + attributes[key] = value; + } + }; + + set(GEN_AI_USAGE_INPUT_TOKENS, sum(usage.input, usage.cacheRead, usage.cacheWrite)); + set(GEN_AI_USAGE_OUTPUT_TOKENS, usage.output); + set(GEN_AI_USAGE_TOTAL_TOKENS, usage.totalTokens); + set(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, usage.cacheRead); + set(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, usage.cacheWrite); + set(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.reasoning); + + set(GEN_AI_COST_INPUT_TOKENS, sum(usage.cost?.input, usage.cost?.cacheRead, usage.cost?.cacheWrite)); + set(GEN_AI_COST_OUTPUT_TOKENS, usage.cost?.output); + set(GEN_AI_COST_TOTAL_TOKENS, usage.cost?.total); + set(GEN_AI_COST_CACHE_READ_INPUT_TOKENS, usage.cost?.cacheRead); + set(GEN_AI_COST_CACHE_CREATION_INPUT_TOKENS, usage.cost?.cacheWrite); + + span.setAttributes(attributes); +} + +function sum(...values: (number | undefined)[]): number | undefined { + const numbers = values.filter((value): value is number => typeof value === 'number'); + return numbers.length ? numbers.reduce((total, value) => total + value, 0) : undefined; +} diff --git a/packages/server-utils/src/ai/types.ts b/packages/server-utils/src/ai/types.ts new file mode 100644 index 000000000000..a2e35318a238 --- /dev/null +++ b/packages/server-utils/src/ai/types.ts @@ -0,0 +1,10 @@ +/** A message in the gen_ai conventions shape, see https://develop.sentry.dev/sdk/telemetry/traces/modules/ai-agents/. */ +export interface GenAiMessage { + role: 'assistant' | 'system' | 'tool' | 'user'; + parts: GenAiMessagePart[]; + finish_reason?: string; +} + +export type GenAiMessagePart = Record & { + type: 'blob' | 'object' | 'reasoning' | 'text' | 'tool_call' | 'tool_call_response'; +}; diff --git a/packages/server-utils/test/ai/lib/tracing/flue.test.ts b/packages/server-utils/test/ai/lib/tracing/flue.test.ts index 5817e1033871..bb3712d6ac32 100644 --- a/packages/server-utils/test/ai/lib/tracing/flue.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/flue.test.ts @@ -338,6 +338,55 @@ describe('createFlueInstrumentation', () => { expect(json?.data['gen_ai.cost.total_tokens']).toBe(0.001199); }); + // pi-ai keeps the cached tokens out of `input`; the conventions count them in. + it('counts cached tokens into the input tokens and their cost into the input cost', async () => { + const usage = { + input: 37, + output: 2, + cacheRead: 52, + cacheWrite: 38, + totalTokens: 129, + cost: { input: 0.000037, output: 0.00001, cacheRead: 0.0000052, cacheWrite: 0.0000475, total: 0.0000997 }, + }; + + await withAgent(() => { + instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); + instrumentation.observe(turn({ response: { ...turn().response, usage } }), {}); + }); + + const json = findSpan('chat claude-haiku-4.5'); + expect(json?.data['gen_ai.usage.input_tokens']).toBe(127); + expect(json?.data['gen_ai.usage.cache_read.input_tokens']).toBe(52); + expect(json?.data['gen_ai.usage.cache_creation.input_tokens']).toBe(38); + expect(json?.data['gen_ai.cost.input_tokens']).toBe(0.000037 + 0.0000052 + 0.0000475); + expect(json?.data['gen_ai.cost.total_tokens']).toBe(0.0000997); + }); + + it('reports a tool-calling turn with the finish reason of the conventions', async () => { + await withAgent(() => { + instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); + instrumentation.observe( + turn({ + response: { + ...turn().response, + finishReason: 'toolUse', + output: { + role: 'assistant', + content: [{ type: 'toolCall', id: 'c1', name: 'get_weather', arguments: { city: 'Berlin' } }], + }, + }, + }), + {}, + ); + }); + + const json = findSpan('chat claude-haiku-4.5'); + expect(json?.data['gen_ai.response.finish_reasons']).toBe('["tool_call"]'); + expect(json?.data['gen_ai.output.messages']).toBe( + '[{"role":"assistant","parts":[{"type":"tool_call","id":"c1","name":"get_weather","arguments":"{\\"city\\":\\"Berlin\\"}"}],"finish_reason":"tool_call"}]', + ); + }); + it('records the model-call tuning and provider endpoint', async () => { await withAgent(() => { instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1', purpose: 'agent' }, {}); @@ -404,7 +453,15 @@ describe('createFlueInstrumentation', () => { await instr.interceptor(AGENT_OP, AGENT_CTX, async () => { instr.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); instr.observe(requestContent, {}); - instr.observe(turn({ response: { ...turn().response, output: { role: 'assistant' } } }), {}); + instr.observe( + turn({ + response: { + ...turn().response, + output: { role: 'assistant', content: [{ type: 'text', text: 'It is sunny.' }] }, + }, + }), + {}, + ); instr.observe( { type: 'tool_start', @@ -415,8 +472,16 @@ describe('createFlueInstrumentation', () => { }, {}, ); + // `result` is the harness-level shape; `effectiveResult` is what the model receives. instr.observe( - { type: 'tool', toolCallId: 'c1', toolName: 'get_weather', result: 'sunny', operationId: 'op_1' }, + { + type: 'tool', + toolCallId: 'c1', + toolName: 'get_weather', + result: { content: [{ type: 'text', text: '"sunny"' }], details: { output: 'sunny' } }, + effectiveResult: 'sunny', + operationId: 'op_1', + }, {}, ); }); @@ -427,8 +492,11 @@ describe('createFlueInstrumentation', () => { const chat = findSpan('chat claude-haiku-4.5'); expect(chat?.data['gen_ai.system_instructions']).toBe('You are helpful.'); - expect(chat?.data['gen_ai.input.messages']).toContain('"role":"user"'); - expect(chat?.data['gen_ai.output.messages']).toContain('"role":"assistant"'); + // Mapped from pi-ai's message shape to the conventions, which is what Sentry renders. + expect(chat?.data['gen_ai.input.messages']).toBe('[{"role":"user","parts":[{"type":"text","content":"hi"}]}]'); + expect(chat?.data['gen_ai.output.messages']).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"It is sunny."}],"finish_reason":"stop"}]', + ); expect(chat?.data['gen_ai.tool.definitions']).toContain('get_weather'); const tool = findSpan('execute_tool get_weather'); @@ -436,6 +504,118 @@ describe('createFlueInstrumentation', () => { expect(tool?.data['gen_ai.tool.call.result']).toBe('sunny'); }); + it('records the content blocks of a tool result that has no effective result', async () => { + await withAgent(() => { + instrumentation.observe( + { type: 'tool_start', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1' }, + {}, + ); + instrumentation.observe( + { + type: 'tool', + toolCallId: 'c1', + toolName: 'get_weather', + result: { + content: [ + { type: 'text', text: 'sunny' }, + { type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }, + ], + details: { customTool: 'get_weather' }, + }, + operationId: 'op_1', + }, + {}, + ); + }); + + expect(findSpan('execute_tool get_weather')?.data['gen_ai.tool.call.result']).toBe( + '[{"type":"text","content":"sunny"},{"type":"blob","mime_type":"image/png"}]', + ); + }); + + it('maps an effective result made of several content blocks', async () => { + await withAgent(() => { + instrumentation.observe( + { type: 'tool_start', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1' }, + {}, + ); + instrumentation.observe( + { + type: 'tool', + toolCallId: 'c1', + toolName: 'get_weather', + effectiveResult: [ + { type: 'text', text: 'sunny' }, + { type: 'image', data: '[image data omitted from event]', mimeType: 'image/png' }, + ], + operationId: 'op_1', + }, + {}, + ); + }); + + expect(findSpan('execute_tool get_weather')?.data['gen_ai.tool.call.result']).toBe( + '[{"type":"text","content":"sunny"},{"type":"blob","mime_type":"image/png"}]', + ); + }); + + it.each([ + ['an empty string', { effectiveResult: '' }], + ['an empty text block', { effectiveResult: [{ type: 'text', text: '' }] }], + ['no content blocks', { result: { content: [], details: {} } }], + ])('records an empty tool result given as %s', async (_label, fields) => { + await withAgent(() => { + instrumentation.observe( + { type: 'tool_start', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1' }, + {}, + ); + instrumentation.observe( + { type: 'tool', toolCallId: 'c1', toolName: 'get_weather', operationId: 'op_1', ...fields }, + {}, + ); + }); + + expect(findSpan('execute_tool get_weather')?.data['gen_ai.tool.call.result']).toBe(''); + }); + + it('maps a tool result in the request and a tool call in the response', async () => { + await withAgent(() => { + instrumentation.observe({ type: 'turn_start', turnId: 'turn_1', operationId: 'op_1' }, {}); + instrumentation.observe( + { + ...requestContent, + request: { + ...requestContent.request, + input: { + messages: [ + { role: 'user', content: 'Weather in Berlin?' }, + { + role: 'assistant', + content: [{ type: 'toolCall', id: 'c1', name: 'get_weather', arguments: { city: 'Berlin' } }], + }, + { + role: 'toolResult', + toolCallId: 'c1', + toolName: 'get_weather', + content: [{ type: 'text', text: 'sunny' }], + isError: false, + }, + ], + }, + }, + }, + {}, + ); + instrumentation.observe(turn(), {}); + }); + + expect( + JSON.parse(String(findSpan('chat claude-haiku-4.5')?.data['gen_ai.input.messages'])).map( + (message: { role: string }) => message.role, + ), + ).toEqual(['user', 'assistant', 'tool']); + }); + it('omits inputs when recordInputs is false but keeps outputs', async () => { await record(createFlueInstrumentation({ recordInputs: false })); diff --git a/packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts b/packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts new file mode 100644 index 000000000000..131b1043ba77 --- /dev/null +++ b/packages/server-utils/test/ai/lib/utils/pi-ai-messages.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; +import { + piAiAssistantMessageToGenAiMessage, + piAiContentToString, + piAiFinishReason, + piAiMessagesToGenAiMessages, +} from '../../../../src/ai/pi-ai/messages'; + +describe('convert pi-ai messages to gen_ai messages', () => { + it('maps user, assistant and tool result messages', () => { + expect( + piAiMessagesToGenAiMessages([ + { role: 'user', content: 'Weather in Vienna?', timestamp: 1 }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'The user wants the weather.' }, + { type: 'text', text: 'Let me check.' }, + { type: 'toolCall', id: 'call_1', name: 'get_weather', arguments: { city: 'Vienna' } }, + ], + stopReason: 'toolUse', + }, + { + role: 'toolResult', + toolCallId: 'call_1', + toolName: 'get_weather', + content: [{ type: 'text', text: 'Sunny in Vienna.' }], + isError: false, + }, + ]), + ).toStrictEqual([ + { role: 'user', parts: [{ type: 'text', content: 'Weather in Vienna?' }] }, + { + role: 'assistant', + parts: [ + { type: 'reasoning', content: 'The user wants the weather.' }, + { type: 'text', content: 'Let me check.' }, + { type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Vienna"}' }, + ], + }, + { + role: 'tool', + parts: [{ type: 'tool_call_response', id: 'call_1', name: 'get_weather', result: 'Sunny in Vienna.' }], + }, + ]); + }); + + it('reports images by media type and drops their base64 data', () => { + expect( + piAiMessagesToGenAiMessages([ + { + role: 'user', + content: [ + { type: 'text', text: 'What is this?' }, + { type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }, + ], + }, + ]), + ).toStrictEqual([ + { + role: 'user', + parts: [ + { type: 'text', content: 'What is this?' }, + { type: 'blob', mime_type: 'image/png' }, + ], + }, + ]); + }); + + it('leaves out system messages, redacted thinking and messages without content', () => { + expect( + piAiMessagesToGenAiMessages([ + { role: 'system', content: '', sections: { preamble: 'You are helpful.' } }, + { role: 'assistant', content: [{ type: 'thinking', thinking: 'c2VjcmV0', redacted: true }] }, + { role: 'user', content: '' }, + 'not a message', + ]), + ).toStrictEqual([]); + }); + + it('leaves out messages with a role the conventions do not have', () => { + expect( + piAiMessagesToGenAiMessages([ + { role: 'custom', content: 'Summary of the earlier turns.' }, + { role: 'user', content: 'Weather in Berlin?' }, + ]), + ).toStrictEqual([{ role: 'user', parts: [{ type: 'text', content: 'Weather in Berlin?' }] }]); + }); + + it('drops empty text blocks, which would hide the tool calls next to them', () => { + expect( + piAiMessagesToGenAiMessages([ + { + role: 'assistant', + content: [ + { type: 'text', text: '' }, + { type: 'text', text: ' \n' }, + { type: 'toolCall', id: 'call_1', name: 'get_weather', arguments: {} }, + ], + }, + { role: 'assistant', content: [{ type: 'text', text: '' }] }, + ]), + ).toStrictEqual([ + { role: 'assistant', parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{}' }] }, + ]); + }); + + it('maps the tool-calling stop reason to the conventions and passes the others through', () => { + expect(piAiFinishReason('toolUse')).toBe('tool_call'); + expect(piAiFinishReason('stop')).toBe('stop'); + expect(piAiFinishReason('aborted')).toBe('aborted'); + expect(piAiFinishReason(undefined)).toBeUndefined(); + expect(piAiFinishReason('')).toBeUndefined(); + }); + + it('keeps unknown part kinds as objects', () => { + expect(piAiMessagesToGenAiMessages([{ role: 'user', content: [{ type: 'audio', url: 'a.mp3' }] }])).toStrictEqual([ + { role: 'user', parts: [{ type: 'object', content: { type: 'audio', url: 'a.mp3' } }] }, + ]); + }); + + it('maps an assistant message to an output message with its finish reason', () => { + expect( + piAiAssistantMessageToGenAiMessage( + { + role: 'assistant', + content: [{ type: 'toolCall', id: 'call_1', name: 'get_weather', arguments: { city: 'Vienna' } }], + }, + 'tool_call', + ), + ).toStrictEqual({ + role: 'assistant', + parts: [{ type: 'tool_call', id: 'call_1', name: 'get_weather', arguments: '{"city":"Vienna"}' }], + finish_reason: 'tool_call', + }); + expect(piAiAssistantMessageToGenAiMessage({ role: 'assistant', content: [] }, 'stop')).toBeUndefined(); + }); + + it('renders text-only content as text and other content as mapped parts', () => { + expect( + piAiContentToString([ + { type: 'text', text: 'line 1' }, + { type: 'text', text: 'line 2' }, + ]), + ).toBe('line 1\nline 2'); + expect(piAiContentToString([{ type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png' }])).toBe( + '[{"type":"blob","mime_type":"image/png"}]', + ); + }); +}); diff --git a/packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts b/packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts new file mode 100644 index 000000000000..6d4b768ba616 --- /dev/null +++ b/packages/server-utils/test/ai/lib/utils/pi-ai-usage.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { setPiAiUsageAttributes } from '../../../../src/ai/pi-ai/usage'; + +function spanRecorder(): { span: Span; attributes: Record } { + const attributes: Record = {}; + return { + span: { setAttributes: (values: Record) => Object.assign(attributes, values) } as unknown as Span, + attributes, + }; +} + +describe('setPiAiUsageAttributes', () => { + it('counts the cached tokens into the input tokens and their cost into the input cost', () => { + const { span, attributes } = spanRecorder(); + + setPiAiUsageAttributes(span, { + input: 37, + output: 20, + cacheRead: 52, + cacheWrite: 38, + reasoning: 8, + totalTokens: 147, + cost: { input: 0.000111, output: 0.0001, cacheRead: 0.0000156, cacheWrite: 0.0001425, total: 0.0003691 }, + }); + + expect(attributes).toStrictEqual({ + 'gen_ai.usage.input_tokens': 127, + 'gen_ai.usage.output_tokens': 20, + 'gen_ai.usage.total_tokens': 147, + 'gen_ai.usage.cache_read.input_tokens': 52, + 'gen_ai.usage.cache_creation.input_tokens': 38, + 'gen_ai.usage.reasoning.output_tokens': 8, + 'gen_ai.cost.input_tokens': 0.000111 + 0.0000156 + 0.0001425, + 'gen_ai.cost.output_tokens': 0.0001, + 'gen_ai.cost.total_tokens': 0.0003691, + 'gen_ai.cost.cache_read.input_tokens': 0.0000156, + 'gen_ai.cost.cache_creation.input_tokens': 0.0001425, + }); + }); + + it('writes only the counters the response has', () => { + const { span, attributes } = spanRecorder(); + + setPiAiUsageAttributes(span, { input: 10, output: 2, totalTokens: 12 }); + + expect(attributes).toStrictEqual({ + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 2, + 'gen_ai.usage.total_tokens': 12, + }); + }); + + it('skips the zero counters of a request that failed before it was billed', () => { + const { span, attributes } = spanRecorder(); + + setPiAiUsageAttributes(span, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 }, true); + setPiAiUsageAttributes(span, undefined); + + expect(attributes).toStrictEqual({}); + }); +}); From cfa5c7577a9fd2a88991b7815b8526c61eabfe3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 6 Oct 2026 10:55:43 +0300 Subject: [PATCH 35/73] fix(bun): Add conversationIdIntegration to the default integrations (#25003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@sentry/bun` builds its own list of default integrations, and that list has no `conversationIdIntegration`. The other runtime SDKs install it (#24299 added it to Deno), and `@sentry/elysia` takes Bun's list. So on Bun and Elysia, a conversation id set with `Sentry.setConversationId()`, or by an AI integration on the scope, never reached the `gen_ai` spans. This was the only reason the bundled Bun runs of the OpenAI and Vercel AI (v4) suites failed, so they run again. Ref #23887 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- .../node-suites/excludes.ts | 2 -- packages/bun/src/sdk.ts | 2 ++ packages/bun/test/init.test.ts | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts index 9705eff5ff1e..d27790a79a0f 100644 --- a/dev-packages/bun-integration-tests/node-suites/excludes.ts +++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts @@ -259,10 +259,8 @@ const BUN_BUILD_NOT_TRIAGED = [ 'suites/tracing/mongoose-v9/test.ts', 'suites/tracing/mongoose/test.ts', 'suites/tracing/mysql/test.ts', - 'suites/tracing/openai/test.ts', 'suites/tracing/prisma-orm-v8/test.ts', 'suites/tracing/together-ai/test.ts', - 'suites/tracing/vercelai/test.ts', 'suites/tracing/vercelai/v6_v7/test.ts', ]; diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index bd936eae1adb..d1aba1b7aebc 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -2,6 +2,7 @@ import * as os from 'node:os'; import type { Integration, Options } from '@sentry/core'; import { applySdkMetadata, + conversationIdIntegration, dedupeIntegration, eventFiltersIntegration, functionToStringIntegration, @@ -51,6 +52,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { linkedErrorsIntegration(), dedupeIntegration(), requestDataIntegration(), + conversationIdIntegration(), // Native Wrappers consoleIntegration(), httpIntegration(), diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts index 8a2929dd7dcf..17983138e5ad 100644 --- a/packages/bun/test/init.test.ts +++ b/packages/bun/test/init.test.ts @@ -1,3 +1,4 @@ +import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; import { getCurrentScope, type Integration } from '@sentry/core'; import * as sentryServerUtils from '@sentry/server-utils'; import type { Mock } from 'bun:test'; @@ -8,6 +9,9 @@ import { getDefaultIntegrationsWithoutPerformance, init, initWithoutDefaultIntegrations, + spanToJSON, + startSpan, + withScope, } from '../src'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -128,6 +132,19 @@ describe('init()', () => { expect(integrations?.map(({ name }) => name)).toContain('Some mock integration 4.1'); expect(integrations?.map(({ name }) => name)).toContain('Some mock integration 4.3'); }); + + it('adds the conversation id of the scope to gen_ai spans', () => { + // The default integrations alone must do it, without the tracing integrations. + mockGetTracingIntegrations.mockImplementation(() => []); + init({ dsn: PUBLIC_DSN, tracesSampleRate: 1 }); + + withScope(scope => { + scope.setConversationId('conversation-1'); + startSpan({ name: 'chat', op: 'gen_ai.chat' }, span => { + expect(spanToJSON(span).attributes[GEN_AI_CONVERSATION_ID]).toBe('conversation-1'); + }); + }); + }); }); describe('runtime', () => { From 24b945d0bb5b7642f2586696943fb6ce952901d2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 10:16:14 +0200 Subject: [PATCH 36/73] test(node): Port GraphQL tracing-channel tests to span streaming (#25055) Port the GraphQL 17 tracing-channel integration suites to span streaming. Part of #24139 --------- Co-authored-by: GPT-6 --- .../graphql-tracing-channel/instrument.mjs | 1 - .../resolvers/instrument-trivial.mjs | 1 - .../resolvers/instrument.mjs | 1 - .../graphql-tracing-channel/resolvers/test.ts | 103 +++++++--- .../tracing/graphql-tracing-channel/test.ts | 181 +++++++++++------- .../instrument-disabled.mjs | 1 - .../instrument.mjs | 1 - .../useOperationNameForRootSpan/test.ts | 39 +++- 8 files changed, 218 insertions(+), 110 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs index ca0a97c22c53..908715027e04 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument-trivial.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs index c69cfdcd4622..aed7c6310653 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts index da29892f852a..911971abf5f9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/resolvers/test.ts @@ -10,47 +10,85 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > resolve spans', () cleanupChildProcesses(); }); - const expectedResolveSpan = (path: string, fieldName: string, parentName: string) => - expect.objectContaining({ - description: `graphql.resolve ${path}`, - op: 'graphql', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining({ - 'graphql.field.name': fieldName, - 'graphql.field.path': path, - 'graphql.parent.name': parentName, - }), - }); - - const EXPECTED_TRANSACTION = { - // Root span renamed with the (sorted) operation names. useOperationNameForRootSpan defaults to true. - transaction: 'Test Transaction (query, query GetUser)', - spans: expect.arrayContaining([ - expect.objectContaining({ description: 'query', op: 'graphql' }), - expect.objectContaining({ description: 'query GetUser', op: 'graphql' }), - expectedResolveSpan('hello', 'hello', 'Query'), - expectedResolveSpan('user', 'user', 'Query'), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('emits resolver spans when ignoreResolveSpans is false', async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createTestRunner() + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + const children = container.items.filter(span => !span.is_segment); + + expect(segment?.name).toBe('Test Transaction'); + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query', 'query GetUser'], + type: 'array', + }); + const executeSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name'] === undefined, + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{ hello }', type: 'string' }); + const getUserSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetUser', + ); + expect(getUserSpan?.name).toBe('GraphQL query'); + expect(getUserSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getUserSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetUser', type: 'string' }); + const helloResolverSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'resolve' && + span.attributes['graphql.field.path']?.value === 'hello', + ); + expect(helloResolverSpan?.name).toBe('GraphQL resolve'); + expect(helloResolverSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(helloResolverSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(helloResolverSpan?.attributes['graphql.field.name']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.field.path']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.parent.name']).toEqual({ value: 'Query', type: 'string' }); + const userResolverSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'resolve' && + span.attributes['graphql.field.path']?.value === 'user', + ); + expect(userResolverSpan?.name).toBe('GraphQL resolve'); + expect(userResolverSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(userResolverSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(userResolverSpan?.attributes['graphql.field.name']).toEqual({ value: 'user', type: 'string' }); + expect(userResolverSpan?.attributes['graphql.field.path']).toEqual({ value: 'user', type: 'string' }); + expect(userResolverSpan?.attributes['graphql.parent.name']).toEqual({ value: 'Query', type: 'string' }); + }, + }) + .start() + .completed(); }); test('skips the default property resolver (trivial resolve) by default', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + // `user.name` uses graphql's default property resolver, so no span is emitted for it. - expect(spans.find(span => span.description === 'graphql.resolve user.name')).toBeUndefined(); + expect(spans.find(span => span.attributes['graphql.field.path']?.value === 'user.name')).toBeUndefined(); // ...but the user-defined resolvers do produce spans. - expect(spans.find(span => span.description === 'graphql.resolve user')).toBeDefined(); + expect(spans.find(span => span.attributes['graphql.field.path']?.value === 'user')).toBeDefined(); }, }) .start() @@ -70,9 +108,12 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > resolve spans', () test('emits a span for the trivial default resolver when ignoreTrivialResolveSpans is false', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - expect(spans.find(span => span.description === 'graphql.resolve user.name')).toBeDefined(); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + expect(spans.find(span => span.attributes['graphql.field.path']?.value === 'user.name')).toBeDefined(); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts index 3347c606e4fb..e1ea0083877a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/test.ts @@ -2,64 +2,84 @@ import { afterAll, expect } from 'vitest'; import { conditionalTest } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -// graphql >= 17 publishes its operations via `node:diagnostics_channel`, so the SDK subscribes to -// those channels (`subscribeGraphqlDiagnosticChannels`) instead of the vendored OTel patcher. This -// suite pins `^17` and asserts the diagnostics-channel path: graphql semconv attributes, redacted -// document text, span relationships, and that the legacy OTel path does NOT also fire (no double -// instrumentation). graphql 17 requires Node >= 22, so this suite is skipped on older Node. +// GraphQL 17 requires Node >= 22, so this suite is skipped on older Node. conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { afterAll(() => { cleanupChildProcesses(); }); - const expectedExecuteSpan = (description: string, extraData: Record = {}) => - expect.objectContaining({ - description, - op: 'graphql', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining(extraData), - }); - - const EXPECTED_TRANSACTION = { - // Root span renamed with the (sorted) operation names. useOperationNameForRootSpan defaults to true. - transaction: 'Test Transaction (mutation Login, query, query Boom, query GetUser)', - spans: expect.arrayContaining([ - expect.objectContaining({ description: 'graphql.parse', op: 'graphql' }), - expect.objectContaining({ description: 'graphql.validate', op: 'graphql' }), - // anonymous query -> span named after the operation type only - expectedExecuteSpan('query', { 'graphql.operation.type': 'query' }), - expectedExecuteSpan('query GetUser', { - 'graphql.operation.type': 'query', - 'graphql.operation.name': 'GetUser', - // the inline `42` literal is redacted out of the document - 'graphql.document': 'query GetUser { user(id: *) { name } }', - }), - expectedExecuteSpan('mutation Login', { - 'graphql.operation.type': 'mutation', - // the inline email literal must be redacted to `"*"`, so the raw value can never leak - 'graphql.document': 'mutation Login { login(email: "*") }', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', (createTestRunner, test) => { test('subscribes to graphql >= 17 diagnostics channels with graphql semconv attributes', async () => { - await createTestRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); - }); - - test('does not double-instrument: the vendored OTel graphql patcher does not fire on 17', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - // The vendored OTel path (origin `auto.graphql.graphql`) must be inactive on 17+. - expect(spans.find(span => span.origin === 'auto.graphql.graphql')).toBeUndefined(); - // ...while the diagnostics-channel path is active. - expect(spans.find(span => span.origin === 'auto.graphql.diagnostic_channel')).toBeDefined(); + span: container => { + const segment = container.items.find(span => span.is_segment); + const children = container.items.filter(span => !span.is_segment); + + expect(segment?.name).toBe('Test Transaction'); + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query', 'query GetUser', 'mutation Login', 'query Boom'], + type: 'array', + }); + const parseSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'parse'); + expect(parseSpan?.name).toBe('GraphQL parse'); + expect(parseSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + const validateSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'validate', + ); + expect(validateSpan?.name).toBe('GraphQL validate'); + expect(validateSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + const executeSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name'] === undefined, + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{ hello }', type: 'string' }); + const getUserSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetUser', + ); + expect(getUserSpan?.name).toBe('GraphQL query'); + expect(getUserSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getUserSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getUserSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getUserSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetUser', type: 'string' }); + expect(getUserSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetUser { user(id: *) { name } }', + type: 'string', + }); + const loginSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Login', + ); + expect(loginSpan?.name).toBe('GraphQL mutation'); + expect(loginSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(loginSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(loginSpan?.attributes['graphql.operation.name']).toEqual({ value: 'Login', type: 'string' }); + expect(loginSpan?.attributes['graphql.operation.type']).toEqual({ value: 'mutation', type: 'string' }); + expect(loginSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Login { login(email: "*") }', + type: 'string', + }); }, }) .start() @@ -69,14 +89,22 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { test('never leaks raw inline literal values into graphql.document', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - for (const span of spans) { - const document = span.data?.['graphql.document']; - if (typeof document === 'string') { - expect(document).not.toContain('secret@example.com'); - } - } + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const loginSpan = spans.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Login', + ); + expect(loginSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Login { login(email: "*") }', + type: 'string', + }); + const documents = spans.map(span => span.attributes['graphql.document']?.value); + expect(documents.join('\n')).not.toContain('secret@example.com'); }, }) .start() @@ -86,25 +114,39 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { test('flags the execute span as errored when a resolver throws', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - const boomSpan = spans.find(span => span.description === 'query Boom'); - expect(boomSpan).toBeDefined(); - expect(boomSpan?.status).toBe('internal_error'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const boomSpan = spans.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Boom', + ); + expect(boomSpan?.status).toBe('error'); + expect(boomSpan?.attributes['sentry.status.message']?.value).toBe('internal_error'); }, }) .start() .completed(); }); - test('parents the execute span to the surrounding transaction', async () => { + test('parents the execute span to the surrounding segment', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - const executeSpan = spans.find(span => span.description === 'query GetUser'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const executeSpan = spans.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetUser', + ); expect(executeSpan).toBeDefined(); - expect(executeSpan?.parent_span_id).toBe(event.contexts?.trace?.span_id); + expect(executeSpan?.parent_span_id).toBe(segment?.span_id); }, }) .start() @@ -124,11 +166,14 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test', () => { test('flags the validate span as errored for an invalid document', async () => { await createTestRunner() .expect({ - transaction: event => { - const spans = event.spans || []; - const validateSpan = spans.find(span => span.description === 'graphql.validate'); - expect(validateSpan).toBeDefined(); - expect(validateSpan?.status).toBe('invalid_argument'); + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + const spans = container.items.filter(span => !span.is_segment); + + const validateSpan = spans.find(span => span.attributes['graphql.processing.type']?.value === 'validate'); + expect(validateSpan?.status).toBe('error'); + expect(validateSpan?.attributes['sentry.status.message']?.value).toBe('invalid_argument'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs index 43403fda0caf..62affdd7a041 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument-disabled.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts index 081a437aeab7..1739309f1e1b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/graphql-tracing-channel/useOperationNameForRootSpan/test.ts @@ -1,4 +1,4 @@ -import { afterAll } from 'vitest'; +import { afterAll, expect } from 'vitest'; import { conditionalTest } from '../../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; @@ -13,9 +13,19 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameFor 'scenario-query.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('renames the root span with a single operation', async () => { + test('records a single operation without renaming the root span', async () => { await createTestRunner() - .expect({ transaction: { transaction: 'Test Transaction (query GetHello)' } }) + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'query GetHello', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -28,9 +38,19 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameFor 'scenario-multiple.mjs', 'instrument.mjs', (createTestRunner, test) => { - test('accumulates multiple operations on the root span name (sorted)', async () => { + test('accumulates multiple operations without renaming the root span', async () => { await createTestRunner() - .expect({ transaction: { transaction: 'Test Transaction (query GetHello, query GetWorld)' } }) + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query GetWorld', 'query GetHello'], + type: 'array', + }); + }, + }) .start() .completed(); }); @@ -45,7 +65,14 @@ conditionalTest({ min: 22 })('GraphQL tracing channel Test > useOperationNameFor (createTestRunner, test) => { test('keeps the original root span name when useOperationNameForRootSpan is false', async () => { await createTestRunner() - .expect({ transaction: { transaction: 'Test Transaction' } }) + .expect({ + span: container => { + const segment = container.items.find(span => span.is_segment); + expect(segment?.name).toBe('Test Transaction'); + + expect(segment?.attributes['sentry.graphql.operation']).toBeUndefined(); + }, + }) .start() .completed(); }); From 95c11468c0188aca1ffcdb1f9b7d44ae1799f3fa Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:25:19 +0200 Subject: [PATCH 37/73] chore(deps): Bump `@sentry/conventions` to 0.26.0 (#25073) `@sentry/conventions` 0.26.0 adds the `GEN_AI_EVALUATE` op constant, so the Vercel AI, TypeSafe and Workers AI integrations now import it instead of hardcoding `gen_ai.evaluate` or building it with `getGenAiSpanOp('evaluate')`. The emitted op is unchanged. Beyond that, the release only adds new exports (no renames or removals), so nothing else needed to change. via **Andrei Borza**. -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC0BKK9QLCUA%3A1791196396.229419) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0BKK9QLCUA%3A1791196396.229419/?project=4510944073809921) --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Andrei Borza --- dev-packages/browser-integration-tests/package.json | 2 +- .../cloudflare-integration-tests/package.json | 2 +- .../test-applications/angular-19/package.json | 2 +- .../test-applications/nuxt-4-static/package.json | 2 +- .../test-applications/solidstart-static/package.json | 2 +- dev-packages/node-integration-tests/package.json | 2 +- packages/angular/package.json | 2 +- packages/astro/package.json | 2 +- packages/aws-serverless/package.json | 2 +- packages/browser-utils/package.json | 2 +- packages/browser/package.json | 2 +- packages/bun/package.json | 2 +- packages/cloudflare/package.json | 2 +- packages/core/package.json | 2 +- packages/deno/package.json | 2 +- packages/effect/package.json | 2 +- packages/elysia/package.json | 2 +- packages/ember/package.json | 2 +- packages/google-cloud-serverless/package.json | 2 +- packages/hono/package.json | 2 +- packages/nestjs/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nitro/package.json | 2 +- packages/node/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/opentelemetry/package.json | 2 +- packages/react-router/package.json | 2 +- packages/react/package.json | 2 +- packages/remix/package.json | 2 +- packages/server-utils/package.json | 2 +- packages/server-utils/src/ai/typesafe/index.ts | 5 +++-- packages/server-utils/src/ai/workers-ai/utils.ts | 11 +++-------- .../integrations/vercel-ai/vercel-ai-dc-subscriber.ts | 4 +--- packages/solid/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/svelte/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- packages/vue/package.json | 2 +- yarn.lock | 8 ++++---- 40 files changed, 47 insertions(+), 53 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 5f53fe3567d1..a62b140e5dd4 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -63,7 +63,7 @@ "@sentry/browser": "11.4.0", "@sentry/replay": "11.4.0", "@sentry/opentelemetry": "11.4.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@supabase/supabase-js": "2.109.0", "axios": "1.20.0", "babel-loader": "^10.1.1", diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 781566e141d4..bdae3c275a3b 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -33,7 +33,7 @@ "@cloudflare/workers-types": "^4.20260426.0", "@cloudflare/workers-types-v5": "npm:@cloudflare/workers-types@5.20260710.1", "@sentry-internal/test-utils": "11.4.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "eslint-plugin-regexp": "^3.1.0", "prisma": "6.15.0", "vite": "^8.3.1", diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json index 417939441749..04bef8b8f52d 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -36,7 +36,7 @@ "@angular/cli": "^19.0.0", "@angular/compiler-cli": "^19.0.0", "@playwright/test": "~1.63.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "file:../../packed/sentry-core-packed.tgz", "@types/jasmine": "~5.1.0", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json index fc4a77a4cedc..d1ac621c81c2 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-static/package.json @@ -26,7 +26,7 @@ }, "devDependencies": { "@playwright/test": "~1.63.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "link:../../../test-utils" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json index 2d4607681775..70257bef6fcc 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-static/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-static/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@playwright/test": "~1.63.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^1.0.0", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index ad564a41bc75..ec46ca451aac 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -108,7 +108,7 @@ }, "devDependencies": { "@opentelemetry/instrumentation-http": "0.220.0", - "@sentry/conventions": "0.25.0", + "@sentry/conventions": "0.26.0", "@sentry-internal/test-utils": "11.4.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", diff --git a/packages/angular/package.json b/packages/angular/package.json index bcccb1ea0a53..d90a603e09fc 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -23,7 +23,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 84578b3f1ec7..80f457529f6d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -53,7 +53,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", "@sentry/server-utils": "11.4.0", diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index e6693785102d..5060f5316552 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -56,7 +56,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index a5ffcf04b7cb..c7fd27419081 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "web-vitals": "^6.2.3" }, "scripts": { diff --git a/packages/browser/package.json b/packages/browser/package.json index bbba7f804b41..6f806161aafd 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@sentry/browser-utils": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/feedback": "11.4.0", "@sentry/replay": "11.4.0", "@sentry/replay-canvas": "11.4.0", diff --git a/packages/bun/package.json b/packages/bun/package.json index 951bdbd8d3f1..6ac2e3fad1a4 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -43,7 +43,7 @@ }, "dependencies": { "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/node": "11.4.0", "@sentry/server-utils": "11.4.0" }, diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index dd47fb96ebc7..1368b018d732 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/opentelemetry": "11.4.0", "@sentry/server-utils": "11.4.0", diff --git a/packages/core/package.json b/packages/core/package.json index dd9e7ffda7e3..f8d6607e58a6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -94,6 +94,6 @@ "zod": "^3.24.1" }, "dependencies": { - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" } } diff --git a/packages/deno/package.json b/packages/deno/package.json index 07d8ea02edb6..8e7676619553 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -28,7 +28,7 @@ ], "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", "@sentry/server-utils": "11.4.0" diff --git a/packages/effect/package.json b/packages/effect/package.json index b6858b5636df..06be83c3a28a 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0" }, diff --git a/packages/elysia/package.json b/packages/elysia/package.json index bf70f6fec0ec..1480039acf3c 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -34,7 +34,7 @@ "dependencies": { "@sentry/bun": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "elysia": "^1.4.0" diff --git a/packages/ember/package.json b/packages/ember/package.json index 2498a3b6d1d1..8fc7337ceb9e 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -45,7 +45,7 @@ "@embroider/addon-shim": "^1.10.2", "@embroider/macros": "^1.20.2", "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "decorator-transforms": "^2.3.2" }, diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 727a74f7b261..def490e4f5d9 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -36,7 +36,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/hono/package.json b/packages/hono/package.json index 9746eaac01f1..d0d6c45cc709 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -74,7 +74,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/server-utils": "11.4.0" }, "peerDependencies": { diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 2f37b1ad369c..126eb82be976 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -44,7 +44,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-utils": "11.4.0" diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index dd1455aa72e8..1f2d91d9a76f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -76,7 +76,7 @@ "@rollup/plugin-commonjs": "28.0.1", "@sentry/browser-utils": "11.4.0", "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/opentelemetry": "11.4.0", diff --git a/packages/nitro/package.json b/packages/nitro/package.json index bee0d4e70df6..7ef2e39fd3d9 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/node/package.json b/packages/node/package.json index ac2983f9092c..2e1a8d5c71ba 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -78,7 +78,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/opentelemetry": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 84bde2dc6d39..cdfefb544c1e 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -56,7 +56,7 @@ "@nuxt/kit": "^4.5.2", "@sentry/browser": "11.4.0", "@sentry/cloudflare": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/bundler-plugins": "11.4.0", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index a4cd97e56a9d..6f8ba9677d45 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -27,7 +27,7 @@ "access": "public" }, "dependencies": { - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0" }, "peerDependencies": { diff --git a/packages/react-router/package.json b/packages/react-router/package.json index db7af0bc228f..4042ab8a4ec0 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -57,7 +57,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/react": "11.4.0", diff --git a/packages/react/package.json b/packages/react/package.json index 0addf1426f26..155b05373528 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,7 +59,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "react": "17.x || 18.x || 19.x", diff --git a/packages/remix/package.json b/packages/remix/package.json index 24a0289d722c..57a70f33c25a 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -78,7 +78,7 @@ "@remix-run/router": "^1.23.4", "@sentry/browser": "11.4.0", "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/react": "11.4.0", diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 759f0f0babc3..d92bab9cd8b9 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -97,7 +97,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0" }, "devDependencies": { diff --git a/packages/server-utils/src/ai/typesafe/index.ts b/packages/server-utils/src/ai/typesafe/index.ts index 0155969192d0..af4115b0137d 100644 --- a/packages/server-utils/src/ai/typesafe/index.ts +++ b/packages/server-utils/src/ai/typesafe/index.ts @@ -10,10 +10,11 @@ import { GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { GEN_AI_EVALUATE } from '@sentry/conventions/op'; import type { Span, SpanAttributes } from '@sentry/core'; import { isObjectLike, SPAN_STATUS_ERROR, startInactiveSpan, stringify, withActiveSpan } from '@sentry/core'; import type { GenAiOptions } from '../core/utils'; -import { getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; +import { resolveAIRecordingOptions } from '../core/utils'; import { TYPESAFE_ORIGIN, TYPESAFE_PROVIDER_NAME } from './constants'; /** @@ -28,7 +29,7 @@ export function startEvaluateSpan(request: unknown, client: unknown, recordInput return startInactiveSpan({ name: model ? `evaluate ${model}` : 'evaluate', - op: getGenAiSpanOp('evaluate'), + op: GEN_AI_EVALUATE, attributes: getRequestAttributes(params, model, recordInputs), }); } diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index b242e62f7014..2ccb9d1125c7 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -16,16 +16,11 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; -import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS } from '@sentry/conventions/op'; +import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS, GEN_AI_EVALUATE } from '@sentry/conventions/op'; import { isObjectLike, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; -import { - extractSystemInstructions, - getGenAiSpanOp, - setOutputMessagesAttribute, - setTokenUsageAttributes, -} from '../core/utils'; +import { extractSystemInstructions, setOutputMessagesAttribute, setTokenUsageAttributes } from '../core/utils'; import { addResponseAttributes as addEvaluateResponseAttributes, getEvaluationInputMessages } from '../typesafe'; // Re-exported so `workers-ai/streaming.ts` keeps importing it from this module. export { setOutputMessagesAttribute }; @@ -37,7 +32,7 @@ export type WorkersAiOperationName = 'chat' | 'embeddings' | 'evaluate'; export const WORKERS_AI_OPERATION_SPAN_OPS: Record = { chat: GEN_AI_CHAT, embeddings: GEN_AI_EMBEDDINGS, - evaluate: getGenAiSpanOp('evaluate'), + evaluate: GEN_AI_EVALUATE, }; /** diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 6923694b3a0c..4daec0d0edf7 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -29,6 +29,7 @@ import { } from '@sentry/conventions/attributes'; import { GEN_AI_EMBEDDINGS, + GEN_AI_EVALUATE, GEN_AI_EXECUTE_TOOL, GEN_AI_GENERATE_CONTENT, GEN_AI_INVOKE_AGENT, @@ -65,9 +66,6 @@ const AI_SDK_TELEMETRY_TRACING_CHANNEL = 'ai:telemetry'; const ORIGIN = 'auto.vercelai.channel'; -// Not yet in `@sentry/conventions`. -const GEN_AI_EVALUATE = 'gen_ai.evaluate'; - // `gen_ai.operation.name` values, keyed to the span op they map to. const GEN_AI_OPERATION_SPAN_OPS = { embeddings: GEN_AI_EMBEDDINGS, diff --git a/packages/solid/package.json b/packages/solid/package.json index 4dbba8f7a941..be95545a57db 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -56,7 +56,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 7562af9e552c..464889441685 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -87,7 +87,7 @@ "@sentry/server-utils": "11.4.0", "@sentry/solid": "11.4.0", "@sentry/bundler-plugins": "11.4.0", - "@sentry/conventions": "0.25.0" + "@sentry/conventions": "0.26.0" }, "devDependencies": { "@solidjs/router": "^1.0.0", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c402014f45b1..edc7ac86dcd1 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@sentry/browser": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "magic-string": "~0.30.0" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 76e8c91f196f..bd5c9caa36bf 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -73,7 +73,7 @@ "@opentelemetry/api": "^1.9.1", "@sentry/cloudflare": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/node": "11.4.0", "@sentry/server-runtime-injection": "11.4.0", "@sentry/server-utils": "11.4.0", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 32a5d8b318d8..8e576bd282fc 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -53,7 +53,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser-utils": "11.4.0", - "@sentry/conventions": "^0.25.0", + "@sentry/conventions": "^0.26.0", "@sentry/core": "11.4.0", "@sentry/node": "11.4.0", "@sentry/react": "11.4.0", diff --git a/packages/vue/package.json b/packages/vue/package.json index 9ebf49932e7b..612745d9fb0b 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -46,7 +46,7 @@ "dependencies": { "@sentry/browser": "11.4.0", "@sentry/core": "11.4.0", - "@sentry/conventions": "^0.25.0" + "@sentry/conventions": "^0.26.0" }, "peerDependencies": { "@tanstack/vue-router": "^1.64.0", diff --git a/yarn.lock b/yarn.lock index 6321817fad73..d324028b3bc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7988,10 +7988,10 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry/conventions@0.25.0", "@sentry/conventions@^0.25.0": - version "0.25.0" - resolved "https://sfw.security.sentry.io/npm/@sentry/conventions/-/conventions-0.25.0.tgz#62c276fe8c608e533a42f0dba2ebc58c62b97b14" - integrity sha512-7SouCAX3qx+AzLsxb6gbl3dKVWNhp2wFjdYQ/T90sGwqPPQUoZVA9YwsWwvx07hqcCiSqFW9mUAzyUftASnuhQ== +"@sentry/conventions@0.26.0", "@sentry/conventions@^0.26.0": + version "0.26.0" + resolved "https://sfw.security.sentry.io/npm/@sentry/conventions/-/conventions-0.26.0.tgz#3da6c91e1c4e570914b998f8b4f97ab7e5144a23" + integrity sha512-ttSRcoLf2kIENDIFzH12aJaVMdxnidMYLvVNwEX/JjXfXaQot8u5M47QmgAFOOhVJ4nQ2uU8wTCh+fwSPrrCfg== "@sentry/node-cpu-profiler@^2.4.4": version "2.4.4" From f2fc75c764a6ff6d9e372dd3a272e6a47000b3c4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Oct 2026 10:38:08 +0200 Subject: [PATCH 38/73] feat(remix): Report the matched route on Remix 3 HTML responses (#25052) The browser has no route table: `remix/component` never matches routes client side and exposes no matched pattern. The server middleware already resolves the pattern for the server span, so it now also reports it on HTML responses through a `Server-Timing: sentry-route` entry, the same channel the Remix 2 SDK and Nitro use for trace data. Nothing reads it yet; the browser SDK does in the next PRs of this stack. Part of #24872 --------- Co-authored-by: Claude Fable 5.1 --- packages/remix/src/v3/routeTiming.ts | 17 +++++ packages/remix/src/v3/server/middleware.ts | 38 +++++++--- packages/remix/test/v3/middleware.test.ts | 83 ++++++++++++++++++++++ packages/remix/test/v3/routeTiming.test.ts | 25 +++++++ 4 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 packages/remix/src/v3/routeTiming.ts create mode 100644 packages/remix/test/v3/middleware.test.ts create mode 100644 packages/remix/test/v3/routeTiming.test.ts diff --git a/packages/remix/src/v3/routeTiming.ts b/packages/remix/src/v3/routeTiming.ts new file mode 100644 index 000000000000..483a867ed8af --- /dev/null +++ b/packages/remix/src/v3/routeTiming.ts @@ -0,0 +1,17 @@ +// The `Server-Timing` entry that carries the matched route pattern from the server to the browser. +// Shared by the server middleware, which writes it, and the browser SDK, which reads it, so the two +// cannot drift. Nothing in here may pull server code into the client bundle. + +export const ROUTE_TIMING_NAME = 'sentry-route'; + +/** The entry for a route, quoted per the header grammar. */ +export function formatRouteTiming(route: string): string { + return `${ROUTE_TIMING_NAME};desc="${route.replace(/["\\]/g, '\\$&')}"`; +} + +/** The route from a raw `Server-Timing` header value, or `undefined` when the entry is absent. */ +export function parseRouteTiming(header: string | null | undefined): string | undefined { + // Everything between the quotes, where a backslash escapes the next character. + const quoted = header?.match(new RegExp(`(?:^|,)\\s*${ROUTE_TIMING_NAME};desc="((?:[^"\\\\]|\\\\.)*)"`))?.[1]; + return quoted?.replace(/\\(.)/g, '$1'); +} diff --git a/packages/remix/src/v3/server/middleware.ts b/packages/remix/src/v3/server/middleware.ts index 541adc696214..7ae9dc989362 100644 --- a/packages/remix/src/v3/server/middleware.ts +++ b/packages/remix/src/v3/server/middleware.ts @@ -10,6 +10,7 @@ import { winterCGRequestToRequestData, } from '@sentry/core'; +import { formatRouteTiming } from '../routeTiming'; import type { MatcherLike, MiddlewareLike, NextFunctionLike, RequestContextLike } from '../types'; import { captureRequestError } from './errorFilter'; import { resolveRoutePattern } from './route'; @@ -27,7 +28,10 @@ export function sentryRemixMiddleware(matcher: MatcherLike): MiddlewareLike { isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(context.request) }); // Applied before `next()` so anything captured while the handler runs already carries the route. - applyRoute(isolationScope, matcher, context); + const route = resolveRoutePattern(matcher, context); + if (route) { + applyRoute(isolationScope, route, context.method); + } let response; try { @@ -40,20 +44,18 @@ export function sentryRemixMiddleware(matcher: MatcherLike): MiddlewareLike { } setResponseStatus(response); + if (route) { + addRouteHeader(response, context.request, route); + } return response; }; } -function applyRoute(isolationScope: Scope, matcher: MatcherLike, context: RequestContextLike): void { - const route = resolveRoutePattern(matcher, context); - if (!route) { - // Nothing matched, so the router falls through to its 404 handler. Leaving the name alone keeps the - // raw URL out of it, which span streaming requires. - return; - } - - const name = `${context.method} ${route}`; +// When nothing matched, the router falls through to its 404 handler. Leaving the name alone keeps the +// raw URL out of it, which span streaming requires. +function applyRoute(isolationScope: Scope, route: string, method: string): void { + const name = `${method} ${route}`; isolationScope.setTransactionName(name); const activeSpan = getActiveSpan(); @@ -67,6 +69,22 @@ function applyRoute(isolationScope: Scope, matcher: MatcherLike, context: Reques rootSpan.setAttribute(HTTP_ROUTE, route); } +/** + * Tells the browser SDK which route served a document, so it can name its page load and navigation + * spans after the pattern. The browser has no route table of its own. Only HTML responses carry it, + * which is what document loads and the runtime's frame fetches ask for. + */ +function addRouteHeader(response: Response, request: Request, route: string): void { + if (!request.headers.get('accept')?.includes('text/html')) { + return; + } + try { + response.headers.append('Server-Timing', formatRouteTiming(route)); + } catch { + // Immutable headers, e.g. a response passed through from `fetch()`. + } +} + function setResponseStatus(response: Response): void { const activeSpan = getActiveSpan(); if (!activeSpan || typeof response?.status !== 'number') { diff --git a/packages/remix/test/v3/middleware.test.ts b/packages/remix/test/v3/middleware.test.ts new file mode 100644 index 000000000000..88c8f1fc42db --- /dev/null +++ b/packages/remix/test/v3/middleware.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest'; + +import { sentryRemixMiddleware } from '../../src/v3/server/middleware'; +import type { MatcherLike, RequestContextLike } from '../../src/v3/types'; + +/** A matcher that resolves every URL to the one pattern, or to nothing. */ +function matcherFor(pattern: string | undefined): MatcherLike { + return { + add: () => {}, + matchAll: () => (pattern ? [{ data: { pattern: { source: pattern }, method: 'GET' }, params: {} }] : []), + }; +} + +function contextFor(url: string, headers: Record = {}): RequestContextLike { + return { request: new Request(url, { headers }), url: new URL(url), method: 'GET', params: {} }; +} + +describe('sentryRemixMiddleware', () => { + it('reports the route on HTML responses through Server-Timing', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/users/:id')); + + const response = await middleware( + contextFor('http://x/users/12345', { accept: 'text/html' }), + async () => new Response('', { headers: { 'content-type': 'text/html' } }), + ); + + expect(response.headers.get('server-timing')).toBe('sentry-route;desc="/users/:id"'); + }); + + it('quotes a pattern that contains header-breaking characters', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/files/:name("a\\b")')); + + const response = await middleware( + contextFor('http://x/files/x', { accept: 'text/html' }), + async () => new Response(''), + ); + + expect(response.headers.get('server-timing')).toBe('sentry-route;desc="/files/:name(\\"a\\\\b\\")"'); + }); + + it('keeps an existing Server-Timing header', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/')); + + const response = await middleware( + contextFor('http://x/', { accept: 'text/html' }), + async () => new Response('', { headers: { 'server-timing': 'db;dur=5' } }), + ); + + expect(response.headers.get('server-timing')).toBe('db;dur=5, sentry-route;desc="/"'); + }); + + it('adds nothing to a response the browser did not ask for as a document', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/api/items/:itemId')); + + const response = await middleware( + contextFor('http://x/api/items/1', { accept: 'application/json' }), + async () => new Response('{}'), + ); + + expect(response.headers.has('server-timing')).toBe(false); + }); + + it('adds nothing when no route matched', async () => { + const middleware = sentryRemixMiddleware(matcherFor(undefined)); + + const response = await middleware( + contextFor('http://x/nope', { accept: 'text/html' }), + async () => new Response('Not Found', { status: 404 }), + ); + + expect(response.headers.has('server-timing')).toBe(false); + }); + + it('leaves a response with immutable headers alone, without failing the request', async () => { + const middleware = sentryRemixMiddleware(matcherFor('/')); + + // `Response.error()` is the one constructor that yields immutable headers, like a passed through + // `fetch()` response does. + const response = await middleware(contextFor('http://x/', { accept: 'text/html' }), async () => Response.error()); + + expect(response.headers.has('server-timing')).toBe(false); + }); +}); diff --git a/packages/remix/test/v3/routeTiming.test.ts b/packages/remix/test/v3/routeTiming.test.ts new file mode 100644 index 000000000000..4e681b686a87 --- /dev/null +++ b/packages/remix/test/v3/routeTiming.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { formatRouteTiming, parseRouteTiming } from '../../src/v3/routeTiming'; + +describe('route timing entry', () => { + it('round trips a route', () => { + expect(parseRouteTiming(formatRouteTiming('/users/:id'))).toBe('/users/:id'); + }); + + it('round trips a route with header-breaking characters', () => { + const route = '/files/:name("a\\b")'; + + expect(formatRouteTiming(route)).toBe('sentry-route;desc="/files/:name(\\"a\\\\b\\")"'); + expect(parseRouteTiming(formatRouteTiming(route))).toBe(route); + }); + + it('finds the entry among others', () => { + expect(parseRouteTiming(`db;dur=5, ${formatRouteTiming('/')}, cache;desc="hit"`)).toBe('/'); + }); + + it('is undefined without the entry', () => { + expect(parseRouteTiming('db;dur=5')).toBeUndefined(); + expect(parseRouteTiming(null)).toBeUndefined(); + }); +}); From 32310b5df65b65a6451f9dbc6fc69569526527e4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Oct 2026 10:38:08 +0200 Subject: [PATCH 39/73] feat(remix): Parameterize Remix 3 page load spans (#25053) The client registers a cached route provider at `init` (#23551) and records the route the server reports for the document, read off the navigation timing entry. The page load span, which starts under the low cardinality fallback, is renamed after it, and the scope's transaction name follows so errors group by route. An app with its own `routeProvider` keeps it. Navigations keep their fallback name here; the next PR names them. Part of #24872 --------- Co-authored-by: Claude Fable 5.1 --- .../remix-v3/app/actions/controller.tsx | 30 ++--- .../remix-v3/tests/client.test.ts | 18 ++- .../v3/client/browserTracingIntegration.ts | 75 ++++++++++++- packages/remix/src/v3/client/routeProvider.ts | 12 ++ packages/remix/src/v3/client/sdk.ts | 3 + packages/remix/src/v3/index.client.ts | 1 + .../test/v3/browserTracingIntegration.test.ts | 106 +++++++++++++++++- packages/remix/test/v3/sdk.test.ts | 15 +++ 8 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 packages/remix/src/v3/client/routeProvider.ts diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx index 69be89b5d961..549a3cd74204 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx @@ -5,18 +5,25 @@ import { ImportMap } from 'remix/component/server'; import { assets, entry } from '../assets.ts'; import { routes } from '../routes.ts'; +// Every document loads the browser entry, so the SDK runs on a direct load of any route. +function Head(handle: Handle<{ title: string }>) { + return () => ( + + + {handle.props.title} + + {entry.preloads.map(href => ( + + ))} + + + ); +} + function HomePage(handle: Handle>) { return () => ( - - - Sentry Remix 3 - - {entry.preloads.map(href => ( - - ))} - - +

Sentry Remix 3

{/* No `data-rmx-document`, so the runtime intercepts this through the Navigation API. */} @@ -37,10 +44,7 @@ function HomePage(handle: Handle>) { function UserPage(handle: Handle<{ id?: string }>) { return () => ( - - - User - +

User {handle.props.id}

diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts index 9428d8b53565..93e8d3251522 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts @@ -9,10 +9,22 @@ test('sends a pageload span', async ({ page }) => { await page.goto('/'); const span = await spanPromise; - // Ordinary document load, so this span comes from the upstream integration and takes the low - // cardinality streaming name. - expect(span.name).toBe('Pageload'); + // Started by the upstream integration, then named after the route the server reported. + expect(span.name).toBe('/'); expect(span.attributes?.['sentry.origin']?.value).toBe('auto.pageload.browser'); + expect(span.attributes?.['sentry.segment.name.source']?.value).toBe('route'); +}); + +test('parameterizes a page load on a route with params', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => getSpanOp(span) === 'pageload' && span.is_segment === true); + + await page.goto('/users/12345'); + + const span = await spanPromise; + expect(span.name).toBe('/users/:id'); + expect(span.name).not.toContain('12345'); + expect(span.attributes?.['sentry.segment.name.source']?.value).toBe('route'); + expect(span.attributes?.['url.template']?.value).toBe('/users/:id'); }); test('sends a navigation span for a link the runtime intercepts', async ({ page }) => { diff --git a/packages/remix/src/v3/client/browserTracingIntegration.ts b/packages/remix/src/v3/client/browserTracingIntegration.ts index 2975aa5e8c81..3104ea3b8128 100644 --- a/packages/remix/src/v3/client/browserTracingIntegration.ts +++ b/packages/remix/src/v3/client/browserTracingIntegration.ts @@ -1,10 +1,24 @@ import { browserTracingIntegration as originalBrowserTracingIntegration, + getActiveSpan, + getCurrentScope, + getRootSpan, + getRouteProvider, + resolveCurrentRoute, startBrowserTracingNavigationSpan, WINDOW, } from '@sentry/browser'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; -import { type Client, hasSpanStreamingEnabled, type Integration, NAVIGATION_SPAN_NAME_FALLBACK } from '@sentry/core'; +import { SENTRY_OP, SENTRY_ORIGIN, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; +import { + type Client, + hasSpanStreamingEnabled, + type Integration, + NAVIGATION_SPAN_NAME_FALLBACK, + type Span, + spanToJSON, +} from '@sentry/core'; + +import { ROUTE_TIMING_NAME } from '../routeTiming'; type Options = Parameters[0]; @@ -14,6 +28,9 @@ type Options = Parameters[0]; * Page loads stay with the upstream integration, because they are ordinary document loads. Navigations * do not: `remix/component` intercepts links and form submissions through the Navigation API and never touches * History, so the upstream handler never fires. + * + * Page load spans are named through the route provider, which learns the route from the document's + * response. */ export function browserTracingIntegration(options: Options = {}): Integration { const integration = originalBrowserTracingIntegration({ ...options, instrumentNavigation: false }); @@ -23,6 +40,9 @@ export function browserTracingIntegration(options: Options = {}): Integration { afterAllSetup(client) { integration.afterAllSetup(client); + if (options.instrumentPageLoad !== false) { + nameActivePageLoadSpan(client); + } if (options.instrumentNavigation !== false) { instrumentNavigationApi(client); } @@ -30,6 +50,25 @@ export function browserTracingIntegration(options: Options = {}): Integration { }; } +/** The page load span the upstream integration just started, named after the route that served the document. */ +function nameActivePageLoadSpan(client: Client): void { + const activeSpan = getActiveSpan(); + if (!activeSpan) { + return; + } + + const rootSpan = getRootSpan(activeSpan); + if (spanToJSON(rootSpan).attributes[SENTRY_OP] !== 'pageload') { + return; + } + + recordRoute(client, WINDOW.location?.pathname, getDocumentRoute()); + const route = resolveCurrentRoute(client); + if (route) { + applyRoute(rootSpan, route); + } +} + function instrumentNavigationApi(client: Client): void { const navigation = (WINDOW as WindowWithNavigation).navigation; if (!navigation) { @@ -46,8 +85,7 @@ function instrumentNavigationApi(client: Client): void { startBrowserTracingNavigationSpan( client, { - // Remix 3 gives the browser no route to name this after: `remix/component` exposes no matched route - // and never matches client side. Passing the server's pattern down is tracked in (#24872). + // Low cardinality: the route arrives with the server's response, which navigations do not read yet. name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathnameOf(url) || '/', attributes: { [SENTRY_SEGMENT_NAME_SOURCE]: 'url', @@ -61,6 +99,35 @@ function instrumentNavigationApi(client: Client): void { }); } +/** Hands a route the server reported to the provider, when it is one that takes them. */ +function recordRoute(client: Client, pathname: string | undefined, route: string | undefined): void { + const provider = getRouteProvider(client); + if (provider && 'record' in provider && typeof provider.record === 'function') { + provider.record(pathname, route); + } +} + +/** + * Renames a span that started under the fallback. The scope's transaction name follows, so errors + * group by route too, but only while this is still the current span. + */ +function applyRoute(span: Span, route: string): void { + span.updateName(route); + span.setAttributes({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', [URL_TEMPLATE]: route }); + const activeSpan = getActiveSpan(); + if (activeSpan && getRootSpan(activeSpan) === span) { + getCurrentScope().setTransactionName(route); + } +} + +/** The route from the document response, read off the navigation timing entry. */ +function getDocumentRoute(): string | undefined { + const entry = WINDOW.performance?.getEntriesByType?.('navigation')[0] as + | { serverTiming?: Array<{ name: string; description: string }> } + | undefined; + return entry?.serverTiming?.find(timing => timing.name === ROUTE_TIMING_NAME)?.description; +} + /** * Whether the runtime will keep this navigation inside the current document. * diff --git a/packages/remix/src/v3/client/routeProvider.ts b/packages/remix/src/v3/client/routeProvider.ts new file mode 100644 index 000000000000..b9bef232cfbc --- /dev/null +++ b/packages/remix/src/v3/client/routeProvider.ts @@ -0,0 +1,12 @@ +import { type CachedRouteProvider, createCachedRouteProvider } from '@sentry/browser'; + +/** + * The route provider for Remix 3. + * + * The browser has no route table: `remix/component` never matches routes client side. The server + * reports the matched pattern on each HTML response instead, and the tracing integration records it + * here as documents and frames arrive. A URL the app has not loaded yet resolves to nothing. + */ +export function createRemixV3RouteProvider(): CachedRouteProvider { + return createCachedRouteProvider(); +} diff --git a/packages/remix/src/v3/client/sdk.ts b/packages/remix/src/v3/client/sdk.ts index 998bfc627a35..edbd2f290545 100644 --- a/packages/remix/src/v3/client/sdk.ts +++ b/packages/remix/src/v3/client/sdk.ts @@ -4,6 +4,7 @@ import { applySdkMetadata, type Client, type Integration } from '@sentry/core'; import { browserTracingIntegration } from './browserTracingIntegration'; import { instrumentClientRuntime } from './errors'; +import { createRemixV3RouteProvider } from './routeProvider'; /** * Default integrations for the Remix 3 client SDK. @@ -19,6 +20,8 @@ export function getDefaultIntegrations(options: BrowserOptions): Integration[] { /** Initialize the Sentry Remix 3 SDK in the browser. */ export function init(options: BrowserOptions): Client | undefined { const opts = { + // Set at `init` so the page load span can be named from it. An app with its own provider keeps it. + routeProvider: createRemixV3RouteProvider(), ...options, defaultIntegrations: options.defaultIntegrations ?? getDefaultIntegrations(options), }; diff --git a/packages/remix/src/v3/index.client.ts b/packages/remix/src/v3/index.client.ts index ffe5c04d3175..66da64fa8581 100644 --- a/packages/remix/src/v3/index.client.ts +++ b/packages/remix/src/v3/index.client.ts @@ -74,4 +74,5 @@ export type { BrowserOptions } from '@sentry/browser'; export { getDefaultIntegrations, init } from './client/sdk'; export { browserTracingIntegration } from './client/browserTracingIntegration'; +export { createRemixV3RouteProvider } from './client/routeProvider'; export { captureRuntimeErrors, instrumentClientRuntime } from './client/errors'; diff --git a/packages/remix/test/v3/browserTracingIntegration.test.ts b/packages/remix/test/v3/browserTracingIntegration.test.ts index 2a78829b1107..7f40994b0f4f 100644 --- a/packages/remix/test/v3/browserTracingIntegration.test.ts +++ b/packages/remix/test/v3/browserTracingIntegration.test.ts @@ -1,19 +1,53 @@ import type { Client } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type * as SentryCore from '@sentry/core'; + +/** The span a start function returns: records the renames the integration applies. */ +function fakeSpan(op: string) { + return { op, updateName: vi.fn(), setAttributes: vi.fn() }; +} +type FakeSpan = ReturnType; + +let activeSpan: FakeSpan | undefined; + const startBrowserTracingNavigationSpan = vi.fn(); const upstreamIntegration = { name: 'BrowserTracing', afterAllSetup: vi.fn() }; const upstreamOptions: { instrumentNavigation?: boolean }[] = []; +/** What `createCachedRouteProvider` does, in miniature: answers from the routes it was told about. */ +const recorded = new Map(); +const routeProvider = { + resolveRoute: (url: { pathname: string }) => recorded.get(url.pathname), + resolveCurrentRoute: () => recorded.get((globalThis as { location?: { pathname: string } }).location?.pathname ?? ''), + record: (pathname: string | undefined, route: string | undefined) => { + if (pathname && route) { + recorded.set(pathname, route); + } + }, +}; +let registeredProvider: typeof routeProvider | undefined = routeProvider; +const scope = { setTransactionName: vi.fn() }; + vi.mock('@sentry/browser', () => ({ browserTracingIntegration: (options: { instrumentNavigation?: boolean }) => { upstreamOptions.push(options); return upstreamIntegration; }, startBrowserTracingNavigationSpan: (...args: unknown[]) => startBrowserTracingNavigationSpan(...args), + getActiveSpan: () => activeSpan, + getCurrentScope: () => scope, + getRootSpan: (span: unknown) => span, + getRouteProvider: () => registeredProvider, + resolveCurrentRoute: () => registeredProvider?.resolveCurrentRoute(), WINDOW: globalThis, })); +vi.mock('@sentry/core', async importOriginal => ({ + ...(await importOriginal()), + spanToJSON: (span: FakeSpan) => ({ attributes: { 'sentry.op': span.op } }), +})); + const { browserTracingIntegration } = await import('../../src/v3/client/browserTracingIntegration'); interface FakeNavigateEvent { @@ -25,11 +59,11 @@ interface FakeNavigateEvent { const client = { getOptions: () => ({ traceLifecycle: 'stream' }) } as unknown as Client; /** Sets up the integration over a fake Navigation API and returns a way to fire `navigate` events. */ -function setup(options = {}): (event: FakeNavigateEvent) => void { +function setup(options = {}, pathname = '/'): (event: FakeNavigateEvent) => void { let listener: ((event: FakeNavigateEvent) => void) | undefined; Object.assign(globalThis, { - location: { origin: 'https://app.test', pathname: '/' }, + location: { origin: 'https://app.test', pathname, href: `https://app.test${pathname}` }, navigation: { addEventListener: (_type: string, fn: (event: FakeNavigateEvent) => void) => { listener = fn; @@ -42,10 +76,24 @@ function setup(options = {}): (event: FakeNavigateEvent) => void { return event => listener?.(event); } +/** A document whose response carried the given route in `Server-Timing`, as the timing entry reports it. */ +function documentWithRoute(route?: string): void { + Object.assign(globalThis, { + performance: { + getEntriesByType: () => [{ serverTiming: route ? [{ name: 'sentry-route', description: route }] : [] }], + }, + }); +} + describe('browserTracingIntegration', () => { beforeEach(() => { startBrowserTracingNavigationSpan.mockClear(); upstreamOptions.length = 0; + activeSpan = undefined; + recorded.clear(); + registeredProvider = routeProvider; + scope.setTransactionName.mockClear(); + documentWithRoute(undefined); }); it('leaves page loads to the upstream integration and takes over navigation', () => { @@ -54,6 +102,58 @@ describe('browserTracingIntegration', () => { expect(upstreamOptions[0]).toEqual({ instrumentNavigation: false }); }); + it('records the route the document reports, so the whole SDK can resolve it', () => { + activeSpan = fakeSpan('pageload'); + documentWithRoute('/users/:id'); + + setup({}, '/users/1'); + + expect(recorded.get('/users/1')).toBe('/users/:id'); + }); + + it("names the page load span after the route the document's Server-Timing reports", () => { + activeSpan = fakeSpan('pageload'); + documentWithRoute('/users/:id'); + + setup({}, '/users/1'); + + expect(activeSpan.updateName).toHaveBeenCalledWith('/users/:id'); + expect(activeSpan.setAttributes).toHaveBeenCalledWith({ + 'sentry.segment.name.source': 'route', + 'url.template': '/users/:id', + }); + // Errors captured from now on group by the route, not the path the span started under. + expect(scope.setTransactionName).toHaveBeenCalledWith('/users/:id'); + }); + + it('leaves the page load span alone when the document reports no route', () => { + activeSpan = fakeSpan('pageload'); + + setup(); + + expect(activeSpan.updateName).not.toHaveBeenCalled(); + expect(scope.setTransactionName).not.toHaveBeenCalled(); + }); + + it('does not rename an active span that is not a page load', () => { + activeSpan = fakeSpan('navigation'); + documentWithRoute('/'); + + setup(); + + expect(activeSpan.updateName).not.toHaveBeenCalled(); + }); + + it('leaves the page load span alone without a provider that records', () => { + registeredProvider = undefined; + activeSpan = fakeSpan('pageload'); + documentWithRoute('/users/:id'); + + setup({}, '/users/1'); + + expect(activeSpan.updateName).not.toHaveBeenCalled(); + }); + it('starts a navigation span for an intercepted navigation', () => { const navigate = setup(); @@ -62,7 +162,7 @@ describe('browserTracingIntegration', () => { expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith( client, { - // Low cardinality, because the browser has no route patterns. + // Low cardinality: navigations do not read the route the server reports yet. name: 'Navigation', attributes: { 'sentry.segment.name.source': 'url', diff --git a/packages/remix/test/v3/sdk.test.ts b/packages/remix/test/v3/sdk.test.ts index 1436966ddb91..f5a0fdca9d3f 100644 --- a/packages/remix/test/v3/sdk.test.ts +++ b/packages/remix/test/v3/sdk.test.ts @@ -52,6 +52,21 @@ describe('the client entry', () => { // `@sentry/browser`'s installs span streaming on first use. Taking one from `@sentry/core` is // invisible until an app replaces the default integrations, because browser tracing installs span // streaming anyway. + it('registers a route provider that takes the routes the server reports', () => { + init({}); + + const { routeProvider } = browserInit.mock.calls.at(-1)?.[0] as { routeProvider: { record: unknown } }; + expect(routeProvider.record).toEqual(expect.any(Function)); + }); + + it("keeps the app's own route provider", () => { + const own = { resolveRoute: () => '/own', resolveCurrentRoute: () => '/own' }; + + init({ routeProvider: own }); + + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining({ routeProvider: own })); + }); + it('re-exports @sentry/browser, never a @sentry/core lookalike', async () => { const [entry, browser] = await Promise.all([ import('../../src/v3/index.client'), From 1e3cccdb87610633f15502d1633c450072c80548 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Oct 2026 10:38:09 +0200 Subject: [PATCH 40/73] feat(remix): Parameterize Remix 3 navigation spans (#25045) A navigation span is named through the route provider: right away when the app loaded the route before, otherwise once the runtime's frame fetch response arrives with the route the server reported. The scope's transaction name follows a late rename only while that span is still the current one. Routes are recorded under the URL that served the response, so a redirected fetch does not misname later visits. Pending spans are keyed by pathname and bounded; a failed fetch leaves its entry alone, because the Navigation API aborts the previous intercept only after a repeated navigation has re-queued the same path. Fixes #24872 --------- Co-authored-by: Claude Fable 5.1 --- .../remix-v3/app/actions/controller.tsx | 3 + .../remix-v3/app/actions/public/entry.ts | 2 +- .../remix-v3/tests/client.test.ts | 46 +++- .../v3/client/browserTracingIntegration.ts | 64 ++++- .../test/v3/browserTracingIntegration.test.ts | 231 +++++++++++++++--- 5 files changed, 302 insertions(+), 44 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx index 549a3cd74204..95b3ac655fec 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/controller.tsx @@ -47,6 +47,9 @@ function UserPage(handle: Handle<{ id?: string }>) {

User {handle.props.id}

+ ); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts index 8a3876dc890b..bbeee228783a 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/app/actions/public/entry.ts @@ -31,7 +31,7 @@ document.addEventListener('click', event => { if (id === 'component-error') { void app.frames.top.replace(createElement(Boom, {})); } - if (id === 'throw-error') { + if (id === 'throw-error' || id === 'throw-on-user') { throwError(); } }); diff --git a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts index 93e8d3251522..70f77fb7627f 100644 --- a/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-v3/tests/client.test.ts @@ -43,7 +43,51 @@ test('sends a navigation span for a link the runtime intercepts', async ({ page const span = await spanPromise; expect(span.is_segment).toBe(true); - expect(span.name).toBe('Navigation'); + // The pattern, not the path: an id in the name would make every navigation its own transaction. + expect(span.name).toBe('/users/:id'); + expect(span.name).not.toContain('12345'); + expect(span.attributes?.['sentry.segment.name.source']?.value).toBe('route'); +}); + +test('names a navigation to a route it loaded before right at the start', async ({ page }) => { + await page.goto('/'); + await page.locator('#to-user').click(); + await expect(page.locator('#user')).toBeVisible(); + await page.goBack(); + await expect(page.locator('#to-user')).toBeVisible(); + + // Named through the route provider before any response, so the span never carried the fallback name. + // Matched on the destination path: the back navigation is a Remix navigation span too, and it can + // be sent when this click starts. + const spanPromise = waitForStreamedSpan( + APP_NAME, + span => + getSpanOp(span) === 'navigation' && + span.attributes?.['sentry.origin']?.value === 'auto.navigation.remix_v3' && + span.attributes?.['url.path']?.value === '/users/12345', + ); + + await page.locator('#to-user').click(); + await expect(page.locator('#user')).toBeVisible(); + + const span = await spanPromise; + expect(span.name).toBe('/users/:id'); + expect(span.attributes?.['url.template']?.value).toBe('/users/:id'); +}); + +test('groups an error after a first visit navigation by the route, not the path', async ({ page }) => { + await page.goto('/'); + await page.locator('#to-user').click(); + await expect(page.locator('#user')).toBeVisible(); + + const errorPromise = waitForError(APP_NAME, event => event.exception?.values?.[0]?.value === 'Remix 3 client error'); + + await page.locator('#throw-on-user').click(); + + const error = await errorPromise; + // The span started under the fallback name and was renamed later; the scope's transaction name + // has to follow, otherwise the error would carry `/users/12345`. + expect(error.transaction).toBe('/users/:id'); }); test('captures a component render error the runtime never rethrows', async ({ page }) => { diff --git a/packages/remix/src/v3/client/browserTracingIntegration.ts b/packages/remix/src/v3/client/browserTracingIntegration.ts index 3104ea3b8128..5e07ca200934 100644 --- a/packages/remix/src/v3/client/browserTracingIntegration.ts +++ b/packages/remix/src/v3/client/browserTracingIntegration.ts @@ -5,11 +5,13 @@ import { getRootSpan, getRouteProvider, resolveCurrentRoute, + resolveRoute, startBrowserTracingNavigationSpan, WINDOW, } from '@sentry/browser'; import { SENTRY_OP, SENTRY_ORIGIN, SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE } from '@sentry/conventions/attributes'; import { + addFetchInstrumentationHandler, type Client, hasSpanStreamingEnabled, type Integration, @@ -18,10 +20,13 @@ import { spanToJSON, } from '@sentry/core'; -import { ROUTE_TIMING_NAME } from '../routeTiming'; +import { parseRouteTiming, ROUTE_TIMING_NAME } from '../routeTiming'; type Options = Parameters[0]; +// The request header the runtime sets on its fetch of a navigation's destination. +const FRAME_REQUEST_HEADER = 'x-remix-frame'; + /** * Browser tracing for Remix 3. * @@ -29,8 +34,9 @@ type Options = Parameters[0]; * do not: `remix/component` intercepts links and form submissions through the Navigation API and never touches * History, so the upstream handler never fires. * - * Page load spans are named through the route provider, which learns the route from the document's - * response. + * Spans are named through the route provider. Routes reach it from the server, with each HTML + * response, so a span starts under a low cardinality name when its route is not known yet and is + * renamed once the response arrives. */ export function browserTracingIntegration(options: Options = {}): Integration { const integration = originalBrowserTracingIntegration({ ...options, instrumentNavigation: false }); @@ -76,19 +82,32 @@ function instrumentNavigationApi(client: Client): void { return; } + // The navigation span still waiting for its route, which arrives with the response to the runtime's + // fetch of the destination. There is at most one: starting a navigation ends the previous span. + let pending: { span: Span; url: string; pathname: string } | undefined; + + client.on('spanEnd', span => { + if (pending?.span === span) { + pending = undefined; + } + }); + navigation.addEventListener('navigate', event => { const url = event.destination?.url; if (!url || !isRuntimeNavigation(event, url)) { return; } - startBrowserTracingNavigationSpan( + const pathname = pathnameOf(url) || '/'; + // Known when the app loaded this route before. + const route = resolveRoute(url, client); + const span = startBrowserTracingNavigationSpan( client, { - // Low cardinality: the route arrives with the server's response, which navigations do not read yet. - name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathnameOf(url) || '/', + name: route ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathname), attributes: { - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [SENTRY_SEGMENT_NAME_SOURCE]: route ? 'route' : 'url', + ...(route && { [URL_TEMPLATE]: route }), [SENTRY_ORIGIN]: 'auto.navigation.remix_v3', }, }, @@ -96,6 +115,33 @@ function instrumentNavigationApi(client: Client): void { // navigation finishes. { url }, ); + + pending = span && !route ? { span, url, pathname } : undefined; + }); + + addFetchInstrumentationHandler(({ fetchData, headers, response }) => { + if (!pending || !response) { + return; + } + // Only the runtime's own fetch of the destination carries the route. The app's fetches, even to + // the same path, do not. + if (headers?.get(FRAME_REQUEST_HEADER) !== 'true') { + return; + } + if (pathnameOf(fetchData.url, WINDOW.location?.href) !== pending.pathname) { + return; + } + const { span, url, pathname } = pending; + pending = undefined; + + // The route belongs to the URL that answered. After a redirect that is not the one requested, and + // recording it under the requested path would misname every later visit there. + const servedPathname = pathnameOf(response.url, WINDOW.location?.href) || pathname; + recordRoute(client, servedPathname, parseRouteTiming(response.headers.get('server-timing'))); + const route = resolveRoute(response.url || url, client); + if (route) { + applyRoute(span, route); + } }); } @@ -148,9 +194,9 @@ function isSameOrigin(url: string): boolean { } } -function pathnameOf(url: string): string | undefined { +function pathnameOf(url: string, base?: string): string | undefined { try { - return new URL(url).pathname; + return new URL(url, base).pathname; } catch { return undefined; } diff --git a/packages/remix/test/v3/browserTracingIntegration.test.ts b/packages/remix/test/v3/browserTracingIntegration.test.ts index 7f40994b0f4f..1c9d660c34b3 100644 --- a/packages/remix/test/v3/browserTracingIntegration.test.ts +++ b/packages/remix/test/v3/browserTracingIntegration.test.ts @@ -9,9 +9,18 @@ function fakeSpan(op: string) { } type FakeSpan = ReturnType; +let navigationSpan: FakeSpan | undefined; let activeSpan: FakeSpan | undefined; - -const startBrowserTracingNavigationSpan = vi.fn(); +let fetchHandler: + | ((data: { + fetchData: { url: string }; + headers?: Headers; + endTimestamp?: number; + response?: { url: string; headers: Headers }; + }) => void) + | undefined; + +const startBrowserTracingNavigationSpan = vi.fn(() => navigationSpan); const upstreamIntegration = { name: 'BrowserTracing', afterAllSetup: vi.fn() }; const upstreamOptions: { instrumentNavigation?: boolean }[] = []; @@ -39,6 +48,7 @@ vi.mock('@sentry/browser', () => ({ getCurrentScope: () => scope, getRootSpan: (span: unknown) => span, getRouteProvider: () => registeredProvider, + resolveRoute: (url: string) => registeredProvider?.resolveRoute(new URL(url, 'https://app.test')), resolveCurrentRoute: () => registeredProvider?.resolveCurrentRoute(), WINDOW: globalThis, })); @@ -46,17 +56,46 @@ vi.mock('@sentry/browser', () => ({ vi.mock('@sentry/core', async importOriginal => ({ ...(await importOriginal()), spanToJSON: (span: FakeSpan) => ({ attributes: { 'sentry.op': span.op } }), + addFetchInstrumentationHandler: (handler: typeof fetchHandler) => { + fetchHandler = handler; + }, })); const { browserTracingIntegration } = await import('../../src/v3/client/browserTracingIntegration'); +/** A response to the runtime's frame fetch, as the fetch instrumentation reports it. */ +function frameResponse(url: string, serverTiming?: string, servedUrl = url) { + return { + fetchData: { url }, + headers: new Headers({ 'x-remix-frame': 'true' }), + endTimestamp: 1, + response: { + url: new URL(servedUrl, 'https://app.test').href, + headers: new Headers(serverTiming ? { 'server-timing': serverTiming } : {}), + }, + }; +} + +/** A frame fetch that failed: settled, but with no response. */ +function failedFrameFetch(url: string) { + return { fetchData: { url }, headers: new Headers({ 'x-remix-frame': 'true' }), endTimestamp: 1 }; +} + interface FakeNavigateEvent { canIntercept: boolean; info?: unknown; destination?: { url: string }; } -const client = { getOptions: () => ({ traceLifecycle: 'stream' }) } as unknown as Client; +let spanEnd: ((span: unknown) => void) | undefined; +const client = { + getOptions: () => ({ traceLifecycle: 'stream' }), + on: (hook: string, callback: (span: unknown) => void) => { + if (hook === 'spanEnd') { + spanEnd = callback; + } + }, +} as unknown as Client; /** Sets up the integration over a fake Navigation API and returns a way to fire `navigate` events. */ function setup(options = {}, pathname = '/'): (event: FakeNavigateEvent) => void { @@ -76,53 +115,73 @@ function setup(options = {}, pathname = '/'): (event: FakeNavigateEvent) => void return event => listener?.(event); } -/** A document whose response carried the given route in `Server-Timing`, as the timing entry reports it. */ -function documentWithRoute(route?: string): void { - Object.assign(globalThis, { - performance: { - getEntriesByType: () => [{ serverTiming: route ? [{ name: 'sentry-route', description: route }] : [] }], - }, - }); -} - describe('browserTracingIntegration', () => { beforeEach(() => { startBrowserTracingNavigationSpan.mockClear(); upstreamOptions.length = 0; + navigationSpan = fakeSpan('navigation'); activeSpan = undefined; + fetchHandler = undefined; recorded.clear(); registeredProvider = routeProvider; scope.setTransactionName.mockClear(); - documentWithRoute(undefined); - }); - - it('leaves page loads to the upstream integration and takes over navigation', () => { - setup(); - - expect(upstreamOptions[0]).toEqual({ instrumentNavigation: false }); + Object.assign(globalThis, { performance: { getEntriesByType: () => [] } }); }); it('records the route the document reports, so the whole SDK can resolve it', () => { activeSpan = fakeSpan('pageload'); - documentWithRoute('/users/:id'); + Object.assign(globalThis, { + performance: { + getEntriesByType: () => [{ serverTiming: [{ name: 'sentry-route', description: '/users/:id' }] }], + }, + }); setup({}, '/users/1'); expect(recorded.get('/users/1')).toBe('/users/:id'); }); + it('names a navigation span from the provider right away when the route is already known', () => { + recorded.set('/users/12345', '/users/:id'); + const navigate = setup(); + + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + + expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith( + client, + expect.objectContaining({ + name: '/users/:id', + attributes: expect.objectContaining({ 'sentry.segment.name.source': 'route', 'url.template': '/users/:id' }), + }), + { url: 'https://app.test/users/12345' }, + ); + }); + + it('still names spans from the response without a provider that records', () => { + registeredProvider = undefined; + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + + fetchHandler?.(frameResponse('/users/12345', 'sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + }); + it("names the page load span after the route the document's Server-Timing reports", () => { activeSpan = fakeSpan('pageload'); - documentWithRoute('/users/:id'); + Object.assign(globalThis, { + performance: { + getEntriesByType: () => [{ serverTiming: [{ name: 'sentry-route', description: '/users/:id' }] }], + }, + }); - setup({}, '/users/1'); + setup(); expect(activeSpan.updateName).toHaveBeenCalledWith('/users/:id'); expect(activeSpan.setAttributes).toHaveBeenCalledWith({ 'sentry.segment.name.source': 'route', 'url.template': '/users/:id', }); - // Errors captured from now on group by the route, not the path the span started under. expect(scope.setTransactionName).toHaveBeenCalledWith('/users/:id'); }); @@ -132,26 +191,132 @@ describe('browserTracingIntegration', () => { setup(); expect(activeSpan.updateName).not.toHaveBeenCalled(); - expect(scope.setTransactionName).not.toHaveBeenCalled(); }); it('does not rename an active span that is not a page load', () => { activeSpan = fakeSpan('navigation'); - documentWithRoute('/'); + Object.assign(globalThis, { + performance: { getEntriesByType: () => [{ serverTiming: [{ name: 'sentry-route', description: '/' }] }] }, + }); setup(); expect(activeSpan.updateName).not.toHaveBeenCalled(); }); - it('leaves the page load span alone without a provider that records', () => { - registeredProvider = undefined; - activeSpan = fakeSpan('pageload'); - documentWithRoute('/users/:id'); + it("names the navigation span after the route in the destination's response", () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + activeSpan = navigationSpan; - setup({}, '/users/1'); + fetchHandler?.(frameResponse('/users/12345', 'db;dur=3, sentry-route;desc="/users/:id"')); - expect(activeSpan.updateName).not.toHaveBeenCalled(); + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/users/:id'); + expect(navigationSpan?.setAttributes).toHaveBeenCalledWith({ + 'sentry.segment.name.source': 'route', + 'url.template': '/users/:id', + }); + // Errors captured from now on group by the route, not the path the span started under. + expect(scope.setTransactionName).toHaveBeenCalledWith('/users/:id'); + }); + + it('drops the response for a navigation whose span already ended', () => { + const first = fakeSpan('navigation'); + const second = fakeSpan('navigation'); + const navigate = setup(); + navigationSpan = first; + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + navigationSpan = second; + navigate({ canIntercept: true, destination: { url: 'https://app.test/items/2' } }); + activeSpan = second; + // Starting the second navigation ended the first span. + spanEnd?.(first); + + fetchHandler?.(frameResponse('/items/2', 'sentry-route;desc="/items/:id"')); + fetchHandler?.(frameResponse('/users/1', 'sentry-route;desc="/users/:id"')); + + expect(second.updateName).toHaveBeenCalledWith('/items/:id'); + expect(first.updateName).not.toHaveBeenCalled(); + expect(scope.setTransactionName).toHaveBeenCalledTimes(1); + expect(scope.setTransactionName).toHaveBeenCalledWith('/items/:id'); + }); + + it('unquotes the route the header carries', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/files/x' } }); + + fetchHandler?.(frameResponse('/files/x', 'sentry-route;desc="/files/:name(\\"a\\\\b\\")"')); + + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/files/:name("a\\b")'); + }); + + it('records a redirected response under the path that served it, not the one requested', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/old/5' } }); + activeSpan = navigationSpan; + + fetchHandler?.(frameResponse('/old/5', 'sentry-route;desc="/users/:id"', '/users/5')); + + expect(recorded.get('/users/5')).toBe('/users/:id'); + expect(recorded.has('/old/5')).toBe(false); + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/users/:id'); + }); + + it('still names a repeated navigation after the aborted first attempt reports its failure', () => { + const first = fakeSpan('navigation'); + const second = fakeSpan('navigation'); + const navigate = setup(); + navigationSpan = first; + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + navigationSpan = second; + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + activeSpan = second; + + // The Navigation API aborted the first intercept; its rejection arrives after the second queued. + fetchHandler?.(failedFrameFetch('/users/1')); + fetchHandler?.(frameResponse('/users/1', 'sentry-route;desc="/users/:id"')); + + expect(second.updateName).toHaveBeenCalledWith('/users/:id'); + expect(first.updateName).not.toHaveBeenCalled(); + }); + + it('forgets the pending span when it ends', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/1' } }); + spanEnd?.(navigationSpan); + + fetchHandler?.(frameResponse('/users/1', 'sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + }); + + it("ignores the app's own fetches during a navigation", () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + + // Same path as the navigation, but without the runtime's frame header: the app's own fetch. + fetchHandler?.({ ...frameResponse('/users/12345', 'sentry-route;desc="/users/:id"'), headers: new Headers() }); + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + + // The navigation is still waiting for its own response. + fetchHandler?.(frameResponse('/users/12345', 'sentry-route;desc="/users/:id"')); + expect(navigationSpan?.updateName).toHaveBeenCalledWith('/users/:id'); + }); + + it('renames a navigation span once, from its own response only', () => { + const navigate = setup(); + navigate({ canIntercept: true, destination: { url: 'https://app.test/users/12345' } }); + fetchHandler?.(frameResponse('/users/12345')); + + fetchHandler?.(frameResponse('/users/12345', 'sentry-route;desc="/users/:id"')); + + expect(navigationSpan?.updateName).not.toHaveBeenCalled(); + }); + + it('leaves page loads to the upstream integration and takes over navigation', () => { + setup(); + + expect(upstreamOptions[0]).toEqual({ instrumentNavigation: false }); }); it('starts a navigation span for an intercepted navigation', () => { @@ -162,7 +327,7 @@ describe('browserTracingIntegration', () => { expect(startBrowserTracingNavigationSpan).toHaveBeenCalledWith( client, { - // Low cardinality: navigations do not read the route the server reports yet. + // Low cardinality until the server reports the route. name: 'Navigation', attributes: { 'sentry.segment.name.source': 'url', @@ -192,7 +357,7 @@ describe('browserTracingIntegration', () => { }); it('names the span after the path when span streaming is off', () => { - const staticClient = { getOptions: () => ({ traceLifecycle: 'static' }) } as unknown as Client; + const staticClient = { getOptions: () => ({ traceLifecycle: 'static' }), on: () => {} } as unknown as Client; let listener: ((event: FakeNavigateEvent) => void) | undefined; Object.assign(globalThis, { location: { origin: 'https://app.test', pathname: '/' }, From 7fa60a41b2e278e2fae4bfd339b20783e1282489 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:41:14 +0000 Subject: [PATCH 41/73] feat(deps): bump source-map-js from 1.2.1 to 1.2.2 (#25076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
Release notes

Sourced from source-map-js's releases.

v1.2.2

  • Fix crash when executing in browser with CSP script-src that don't permit unsafe-eval (#29) @​xfournet

  • Fix denial of service from malicious indexed source maps (CVE-2026-93749) (#79)

    Reported by @​waydeshi in #76. A fix was also proposed by @​aniebiet in #78.

Changelog

Sourced from source-map-js's changelog.

1.2.2

  • Fix crash when executing in browser with CSP script-src that don't permit unsafe-eval (#29) @​xfournet

  • Fix denial of service from malicious indexed source maps (CVE-2026-93749) (#79)

    Reported by @​waydeshi in #76. A fix was also proposed by @​aniebiet in #78.

Commits
  • 0a1d334 1.2.2
  • 4c6fa26 Update changelog
  • cf76580 Fix denial of service from malicious indexed source maps (CVE-2026-93749) (#79)
  • 7899a86 Fix crash when executing browser with CSP script-src that don't permit unsafe...
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=source-map-js&package-manager=npm_and_yarn&previous-version=1.2.1&new-version=1.2.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d324028b3bc9..fef2eb283192 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25254,9 +25254,9 @@ sorted-array-functions@^1.3.0: integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA== "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + version "1.2.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab" + integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw== source-map-loader@4.0.0: version "4.0.0" From c8805d82391269612b61ae319022df7387c36f2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:50:48 +0000 Subject: [PATCH 42/73] feat(deps): bump proxy-addr from 2.0.7 to 2.0.8 (#25077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [proxy-addr](https://github.com/jshttp/proxy-addr) from 2.0.7 to 2.0.8.
Release notes

Sourced from proxy-addr's releases.

2.0.8

Important

What's Changed

New Contributors

Full Changelog: https://github.com/jshttp/proxy-addr/compare/v2.0.7...v2.0.8

Changelog

Sourced from proxy-addr's changelog.

2.0.8

Commits
  • a11ad82 2.0.8 (#70)
  • 780911d fix: reject IPv4 trust via mapped IPv6 subnets with a short prefix
  • 92e103e fix(ci): use publised as release trigger event (#71)
  • 3e5ac75 ci: merge coverage via artifacts, disable fail-fast, add Node.js 23-26 (#69)
  • 4b9db81 chore(ci): npm-publish via workflows (#54)
  • 655e895 build(deps-dev): bump eslint-plugin-import from 2.31.0 to 2.32.0 (#39)
  • 50ce4d0 build(deps): bump ossf/scorecard-action from 2.4.2 to 2.4.3 (#45)
  • 0fd347f build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#63)
  • 6a517fa build(deps): bump github/codeql-action from 4.32.4 to 4.36.0 (#64)
  • 0d45e2a Fix "arugment" typo in README (#61)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for proxy-addr since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=proxy-addr&package-manager=npm_and_yarn&previous-version=2.0.7&new-version=2.0.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fef2eb283192..0c17c12ada19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23208,9 +23208,9 @@ property-information@^7.0.0: integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== proxy-addr@^2.0.7, proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932" + integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ== dependencies: forwarded "0.2.0" ipaddr.js "1.9.1" From d79f44cbe78f84f91b846ae04a8f3c40a5b5bb18 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:59:01 +0200 Subject: [PATCH 43/73] fix(nuxt): Do not inject debug IDs into the SSR build (#25080) Nuxt server source maps can still resolve to wrong lines (more info in #18519). Nitro bundles Nuxt's SSR build again. The Sentry Vite plugin also ran on the SSR build, so its debug IDs got into Nitro's output. Many server files used such an ID instead of Nitro's own, and both builds uploaded different maps under that ID. The Vite plugin now runs only on the client build. Nitro's plugin uploads the server maps, which already point to the original sources. The e2e test now checks that each shipped file has one debug ID, resolving to exactly one map generated for that file. Fixes #18974 --- .../nuxt-4-sourcemaps/assert-build.ts | 34 ++++++++++ dev-packages/test-utils/src/build-output.ts | 62 +++++++++++++++++-- dev-packages/test-utils/src/index.ts | 2 + .../test-utils/src/sourcemap-upload-utils.ts | 28 ++++++++- packages/nuxt/src/vite/sourceMaps.ts | 19 +++--- .../test/vite/sourceMaps-nuxtHooks.test.ts | 56 ++++++++--------- 6 files changed, 160 insertions(+), 41 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts index 14d64149cefc..49b9839d98ba 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4-sourcemaps/assert-build.ts @@ -2,6 +2,9 @@ import * as assert from 'assert/strict'; import * as fs from 'fs'; import * as path from 'path'; import { + findDebugIdsWithConflictingSourcemaps, + findFilesWithForeignSourcemaps, + findFilesWithMultipleDebugIds, findInjectedDebugIds, findSourceMapFiles, findSourceMappingUrlComments, @@ -9,6 +12,7 @@ import { getAssembleRequests, getChunkUploadPosts, getDebugIdPairs, + getSourcemapSources, getSourcemaps, loadMockServerResults, } from '@sentry-internal/test-utils'; @@ -173,6 +177,36 @@ for (const outputDir of isStaticBuild ? [CLIENT_OUTPUT] : [CLIENT_OUTPUT, SERVER assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded sourcemap`); console.log(` ${outputDir}: ${injectedDebugIds.length} injected debug ID(s), all uploaded`); + + // Nitro bundles the SSR build again, so its debug IDs can end up next to Nitro's own. + // Reference issue: https://github.com/getsentry/sentry-javascript/issues/18519 + const filesWithMultipleDebugIds = findFilesWithMultipleDebugIds({ outputDir }); + assert.deepEqual(filesWithMultipleDebugIds, [], `Expected at most one debug ID per file in ${outputDir}`); + + // A server file can borrow the debug ID of its SSR build chunk. Then the upload check above passes on + // Vite's map even if Nitro uploaded nothing. + const filesWithForeignSourcemaps = findFilesWithForeignSourcemaps({ outputDir }, sourcemaps); + assert.deepEqual( + filesWithForeignSourcemaps, + [], + `Expected the source map uploaded for each file in ${outputDir} to be generated for that file`, + ); +} + +// If both builds upload a map under a borrowed debug ID, Sentry can resolve with the wrong one. +const conflictingDebugIds = findDebugIdsWithConflictingSourcemaps(sourcemaps); +assert.deepEqual(conflictingDebugIds, [], 'Expected every uploaded debug ID to resolve to exactly one source map'); + +if (!isStaticBuild) { + // Nitro bundles Vite's SSR output, so server frames reach `.vue` files only through the maps of that build. + const serverDebugIds = new Set(findInjectedDebugIds({ outputDir: SERVER_OUTPUT })); + const serverSources = sourcemaps + .filter(map => map.debugId && serverDebugIds.has(map.debugId)) + .flatMap(map => getSourcemapSources(map.sourcemap)); + assert.ok( + serverSources.some(source => source.endsWith('app/pages/index.vue')), + `Expected the server source maps to resolve to app/pages/index.vue, got: ${serverSources.join(', ')}`, + ); } console.log(''); diff --git a/dev-packages/test-utils/src/build-output.ts b/dev-packages/test-utils/src/build-output.ts index a3c2445e9f8a..f42e5dd1bdb0 100644 --- a/dev-packages/test-utils/src/build-output.ts +++ b/dev-packages/test-utils/src/build-output.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import type { SourcemapEntry } from './sourcemap-upload-utils'; export interface OutputScanOptions { /** Directory holding the emitted bundles, e.g. `/.output/public`. */ @@ -18,6 +19,12 @@ const SOURCE_MAPPING_URL_PATTERN = /[#@]\s*sourceMappingURL\s*=\s*(?!data:)(\S+) /** The `sentry-dbid-` identifier the bundler plugin injects alongside `_sentryDebugIds`. */ const INJECTED_DEBUG_ID_PATTERN = /sentry-dbid-([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})/gi; +/** Distinct lowercased debug IDs in `file`, in order of appearance. */ +function readDebugIds(file: string): string[] { + const matches = fs.readFileSync(file, 'utf8').matchAll(INJECTED_DEBUG_ID_PATTERN); + return [...new Set([...matches].map(match => (match[1] as string).toLowerCase()))]; +} + function* walkFiles(outputDir: string, extensions?: string[]): Generator { if (!fs.existsSync(outputDir)) { throw new Error(`[build-output] Output directory does not exist: ${outputDir}`); @@ -147,12 +154,59 @@ export function findInjectedDebugIds({ outputDir, extensions = JS_EXTENSIONS }: const debugIds = new Set(); for (const file of walkFiles(outputDir, extensions)) { - const contents = fs.readFileSync(file, 'utf8'); + readDebugIds(file).forEach(debugId => debugIds.add(debugId)); + } + + return [...debugIds]; +} + +/** + * Returns ` → ` for each file with more than one debug ID. + * The uploader reads the first ID, but the SDK can report any of them. + */ +export function findFilesWithMultipleDebugIds({ + outputDir, + buildDir = process.cwd(), + extensions = JS_EXTENSIONS, +}: OutputScanOptions): string[] { + const offenders: string[] = []; + + for (const file of walkFiles(outputDir, extensions)) { + const debugIds = readDebugIds(file); - for (const match of contents.matchAll(INJECTED_DEBUG_ID_PATTERN)) { - debugIds.add((match[1] as string).toLowerCase()); + if (debugIds.length > 1) { + offenders.push(`${path.relative(buildDir, file)} → ${debugIds.join(', ')}`); } } - return [...debugIds]; + return offenders; +} + +/** + * Returns ` → ` for each file where no map uploaded under its first debug ID names it in `file`. + * Skips files without a debug ID or without an uploaded map that has `file`. + */ +export function findFilesWithForeignSourcemaps( + { outputDir, buildDir = process.cwd(), extensions = JS_EXTENSIONS }: OutputScanOptions, + uploadedSourcemaps: SourcemapEntry[], +): string[] { + const offenders: string[] = []; + + for (const file of walkFiles(outputDir, extensions)) { + const [debugId] = readDebugIds(file); + if (!debugId) { + continue; + } + + const mapFiles = uploadedSourcemaps + .filter(entry => entry.debugId === debugId) + .map(entry => entry.sourcemap.file) + .filter(mapFile => mapFile !== undefined); + + if (mapFiles.length > 0 && !mapFiles.includes(path.basename(file))) { + offenders.push(`${path.relative(buildDir, file)} → ${mapFiles.join(', ')}`); + } + } + + return offenders; } diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index e1b46f8a5818..3da52584017a 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -23,6 +23,8 @@ export { findSourceMapFiles, findSourceMappingUrlComments, findInjectedDebugIds, + findFilesWithMultipleDebugIds, + findFilesWithForeignSourcemaps, bundleReferencesModule, } from './build-output'; export type { OutputScanOptions } from './build-output'; diff --git a/dev-packages/test-utils/src/sourcemap-upload-utils.ts b/dev-packages/test-utils/src/sourcemap-upload-utils.ts index 88ee298824ac..559169f05bfc 100644 --- a/dev-packages/test-utils/src/sourcemap-upload-utils.ts +++ b/dev-packages/test-utils/src/sourcemap-upload-utils.ts @@ -58,6 +58,7 @@ export interface ArtifactBundleData { export interface ParsedSourcemap { [key: string]: unknown; version?: number; + file?: string; sources?: string[]; /** Absent when the generator drops all sources (Rollup's `sourcemapExcludeSources`), null per dropped entry. */ sourcesContent?: (string | null)[]; @@ -68,6 +69,8 @@ export interface ParsedSourcemap { export interface SourcemapEntry { url: string; bundleDir: string; + /** Lowercased `debug-id` manifest header. */ + debugId?: string; sourcemap: ParsedSourcemap; } @@ -168,6 +171,28 @@ export function getDebugIdPairs(bundles: ArtifactBundleData[]): DebugIdPair[] { return pairs; } +/** + * Returns ` → ` for each debug ID uploaded with maps that differ in `sources` or `mappings`. + * Byte-identical chunks share a debug ID, so the `file` field is not compared. + */ +export function findDebugIdsWithConflictingSourcemaps(sourcemaps: SourcemapEntry[]): string[] { + const mapsByDebugId = new Map>(); + + for (const { url, debugId, sourcemap } of sourcemaps) { + if (!debugId) continue; + + const mappingKey = JSON.stringify([getSourcemapSources(sourcemap), sourcemap.mappings]); + + const maps = mapsByDebugId.get(debugId) ?? new Map(); + maps.set(mappingKey, sourcemap.file ?? url); + mapsByDebugId.set(debugId, maps); + } + + return [...mapsByDebugId] + .filter(([, maps]) => maps.size > 1) + .map(([debugId, maps]) => `${debugId} → ${[...maps.values()].join(', ')}`); +} + /** * Read and parse all sourcemap files from artifact bundles. */ @@ -195,7 +220,8 @@ export function getSourcemaps(bundles: ArtifactBundleData[]): SourcemapEntry[] { continue; } - sourcemaps.push({ url: entry.url, bundleDir, sourcemap }); + const debugId = getManifestHeader(entry.headers, 'debug-id')?.toLowerCase(); + sourcemaps.push({ url: entry.url, bundleDir, debugId, sourcemap }); } } diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index de7d7bb00bdc..d03c725e6c8c 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -22,7 +22,7 @@ export type SourceMapSetting = boolean | 'hidden' | 'inline'; export function setupSourceMaps( moduleOptions: SentryNuxtModuleOptions, nuxt: Nuxt, - addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean }) => void, + addVitePlugin: (plugin: Plugin[], options?: { dev?: boolean; build?: boolean; server?: boolean }) => void, ): void { // Warn here rather than in `getPluginOptions`, which runs once per bundler (Vite and Nitro's // Rollup) and would emit the same warning twice. @@ -73,13 +73,16 @@ export function setupSourceMaps( }); if (sourceMapsEnabled && !nuxt.options.dev && !nuxt.options?._prepare) { + // Also on the SSR build: Nitro's source maps reach the original files only through that build's maps. + addVitePlugin([validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled })], { + dev: false, + build: true, + }); + addVitePlugin( - [ - validateSourceMapsOptionsPlugin({ nuxt, moduleOptions, sourceMapsEnabled }), - // Vite plugin is added on the client and server side (plugin runs for both builds) - ...sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))), - ], - { dev: false, build: true }, // Only add source map plugin during build + sentryVitePlugin(withoutSourceMapDeletion(getPluginOptions(moduleOptions, shouldDeleteFilesFallback))), + // Nitro bundles the SSR output again, so debug IDs from the SSR build would point to the wrong maps. + { dev: false, build: true, server: false }, ); } @@ -162,7 +165,7 @@ export function getPluginOptions( sourcemaps: { disable: moduleOptions.sourcemaps?.disable, // 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). + // We cannot determine automatically how the build folder looks like (depends on the preset), so each plugin uploads its own build output (the vitePlugin for the Nuxt client 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/**/*'], assets: sourcemapsOptions.assets ?? undefined, ignore: sourcemapsOptions.ignore ?? undefined, diff --git a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts index 2ef55c0959ec..18f67df7ab40 100644 --- a/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts +++ b/packages/nuxt/test/vite/sourceMaps-nuxtHooks.test.ts @@ -22,16 +22,16 @@ vi.mock('@sentry/bundler-plugins/vite', () => ({ sentryVitePlugin: mockSentryVit vi.mock('@sentry/bundler-plugins/rollup', () => ({ sentryRollupPlugin: mockSentryRollupPlugin })); function createMockAddVitePlugin() { - let capturedPlugins: Plugin[] | null = null; + const capturedPlugins: Plugin[] = []; const mockAddVitePlugin = vi.fn((plugins: Plugin[]) => { - capturedPlugins = plugins; + capturedPlugins.push(...plugins); }); return { mockAddVitePlugin, - getCapturedPlugin: () => capturedPlugins?.[0] ?? null, - getCapturedPlugins: () => capturedPlugins, + getValidationPlugin: () => + capturedPlugins.find(plugin => plugin.name === 'sentry-nuxt-source-map-validation') ?? null, }; } @@ -121,15 +121,30 @@ describe('setupSourceMaps hooks', () => { }); describe('vite plugin registration', () => { - it('calls `addVitePlugin` when setupSourceMaps is called', async () => { + it('adds the source map validation plugin to the client and server builds', async () => { const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - const plugin = getCapturedPlugin(); - expect(plugin).not.toBeNull(); - expect(plugin?.name).toBe('sentry-nuxt-source-map-validation'); + expect(mockAddVitePlugin).toHaveBeenCalledWith( + [expect.objectContaining({ name: 'sentry-nuxt-source-map-validation' })], + { dev: false, build: true }, + ); + }); + + it('adds the Sentry Vite plugin to the client build only', async () => { + const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); + const { mockAddVitePlugin } = createMockAddVitePlugin(); + + setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); + + expect(mockAddVitePlugin).toHaveBeenCalledTimes(2); + expect(mockAddVitePlugin).toHaveBeenCalledWith([{ name: 'sentry-vite-plugin' }], { + dev: false, + build: true, + server: false, + }); }); it.each([ @@ -160,21 +175,6 @@ describe('setupSourceMaps hooks', () => { expect(mockAddVitePlugin).not.toHaveBeenCalled(); }); - - it.each([ - { label: 'server (SSR) build', buildConfig: { build: { ssr: true }, plugins: [] } }, - { label: 'client build', buildConfig: { build: { ssr: false }, plugins: [] } }, - ])('adds sentry vite plugin to vite config for $label in production', async () => { - const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugins } = createMockAddVitePlugin(); - - setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); - - const plugins = getCapturedPlugins(); - expect(plugins).not.toBeNull(); - expect(plugins?.length).toBeGreaterThan(0); - expect(mockSentryVitePlugin).toHaveBeenCalled(); - }); }); describe('sentry vite plugin calls', () => { @@ -372,12 +372,12 @@ describe('setupSourceMaps hooks', () => { describe('debug logging', () => { it('logs a [Sentry] message in production mode', async () => { const mockNuxt = createMockNuxt({ _prepare: false, dev: false }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); + const plugin = getValidationPlugin(); if (plugin && typeof plugin.config === 'function') { (plugin.config as (config: UserConfig, env: ConfigEnv) => void)( { build: { ssr: false }, plugins: [] } as UserConfig, @@ -398,12 +398,12 @@ describe('setupSourceMaps hooks', () => { it('does not log a [Sentry] messages in prepare mode', async () => { const mockNuxt = createMockNuxt({ _prepare: true }); - const { mockAddVitePlugin, getCapturedPlugin } = createMockAddVitePlugin(); + const { mockAddVitePlugin, getValidationPlugin } = createMockAddVitePlugin(); setupSourceMaps({ debug: true }, mockNuxt as unknown as Nuxt, mockAddVitePlugin); await mockNuxt.triggerHook('modules:done'); - const plugin = getCapturedPlugin(); + const plugin = getValidationPlugin(); if (plugin && typeof plugin.config === 'function') { (plugin.config as (config: UserConfig, env: ConfigEnv) => void)({ build: {}, plugins: [] } as UserConfig, { mode: 'production', From 6ba1c6811b0eefd1038a2b63c400978114295f59 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:27:54 +0200 Subject: [PATCH 44/73] test(e2e): Raise mock Sentry server chunk size to fit whole bundles (#25097) ## What The mock Sentry server now asks the CLI to upload each artifact bundle as one chunk. ## Why Nuxt 4.6 makes the server bundle of `nuxt-4-sourcemaps` larger than 8 MiB. The CLI then splits it into chunks that the mock server cannot read, so the static variant fails on every PR. Co-authored-by: Claude Opus 5.5 --- dev-packages/test-utils/src/mock-sentry-server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-packages/test-utils/src/mock-sentry-server.ts b/dev-packages/test-utils/src/mock-sentry-server.ts index 17390e74c30d..0e59dc0ac4fa 100644 --- a/dev-packages/test-utils/src/mock-sentry-server.ts +++ b/dev-packages/test-utils/src/mock-sentry-server.ts @@ -171,7 +171,9 @@ function sendResponse( res.end( JSON.stringify({ url: `http://localhost:${port}/api/0/organizations/${org}/chunk-upload/`, - chunkSize: 8388608, + // Each chunk is unzipped on its own, so a bundle split across chunks loses its manifest. + // Keep chunks as large as a request allows so that every bundle fits in one chunk. + chunkSize: 33554432, chunksPerRequest: 64, maxFileSize: 2147483648, maxRequestSize: 33554432, From d308ed77cc2da67cfc308f676794a6fda19b38b4 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:47:31 +0200 Subject: [PATCH 45/73] chore(github): Remove triage-issue workflow (#25098) Removes the `Triage Issue` GitHub Actions workflow (`.github/workflows/triage-issue.yml`). It is no longer needed. via **Charly Gomez**. -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC0C55JYA84F%3A1790779997.226189) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0C55JYA84F%3A1790779997.226189/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Charly Gomez --- .github/workflows/triage-issue.yml | 98 ------------------------------ 1 file changed, 98 deletions(-) delete mode 100644 .github/workflows/triage-issue.yml diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml deleted file mode 100644 index 78ec8b6586e5..000000000000 --- a/.github/workflows/triage-issue.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Triage Issue - -on: - issues: - types: [opened] - workflow_dispatch: - inputs: - issue_number: - description: 'Issue number (e.g., 1234)' - required: true - type: number - -# Per-issue concurrency to prevent duplicate analysis -concurrency: - group: triage-issue-${{ github.event.issue.number || github.event.inputs.issue_number }} - cancel-in-progress: false - -jobs: - triage-issue: - runs-on: ubuntu-latest - environment: ci-triage - permissions: - contents: read - issues: read - pull-requests: read - id-token: write - # Only run for Bug or Feature issues (automatic mode) - if: | - github.event_name == 'workflow_dispatch' || - contains(github.event.issue.labels.*.name, 'Bug') || - contains(github.event.issue.labels.*.name, 'Feature') - - steps: - - name: Parse issue number - id: parse-issue - env: - EVENT_NAME: ${{ github.event_name }} - EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - INPUT_ISSUE_NUMBER: ${{ github.event.inputs.issue_number }} - run: | - if [ "$EVENT_NAME" = "issues" ]; then - ISSUE_NUM="$EVENT_ISSUE_NUMBER" - else - ISSUE_NUM="$INPUT_ISSUE_NUMBER" - fi - - echo "issue_number=$ISSUE_NUM" >> "$GITHUB_OUTPUT" - echo "Processing issue #$ISSUE_NUM in CI mode" - - - name: Checkout repository - uses: actions/checkout@v7 - with: - ref: develop - - - name: Run Claude triage - id: triage - uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1 - env: - ANTHROPIC_BASE_URL: https://openrouter.ai/api - with: - anthropic_api_key: ${{ secrets.OPENROUTER_API_KEY }} - github_token: ${{ secrets.GITHUB_TOKEN }} - allowed_non_write_users: '*' - settings: | - { - "env": { - "LINEAR_CLIENT_ID": "${{ secrets.LINEAR_CLIENT_ID }}", - "LINEAR_CLIENT_SECRET": "${{ secrets.LINEAR_CLIENT_SECRET }}" - } - } - prompt: | - /triage-issue ${{ steps.parse-issue.outputs.issue_number }} --ci - IMPORTANT: Do NOT wait for approval. - Do NOT write to `/tmp/` or any other directory. Only write files (e.g. triage_report.md) inside the workspace (repo root). - Do NOT use Bash redirection (> file)—it is blocked. - Do NOT use `python3 -c` or other inline Python in Bash, only the provided scripts are allowed. - Do NOT attempt to delete (`rm`) temporary files you create. - claude_args: | - --model claude-opus-4-8 - --max-turns 50 --allowedTools "Write,Bash(gh api *),Bash(gh pr list *),Bash(npm info *),Bash(npm ls *),Bash(python3 .claude/skills/triage-issue/scripts/post_linear_comment.py *),Bash(python3 .claude/skills/triage-issue/scripts/parse_gh_issues.py *),Bash(python3 .claude/skills/triage-issue/scripts/detect_prompt_injection.py *),Bash(python3 .claude/skills/triage-issue/scripts/write_job_summary.py *)" - - - name: Post triage job summary - if: always() - run: | - EXEC_FILE="${{ steps.triage.outputs.execution_file }}" - if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then - EXEC_FILE="${RUNNER_TEMP}/claude-execution-output.json" - fi - if [ ! -f "$EXEC_FILE" ]; then - EXEC_FILE="${GITHUB_WORKSPACE}/../../_temp/claude-execution-output.json" - fi - if [ -f "$EXEC_FILE" ]; then - python3 .claude/skills/triage-issue/scripts/write_job_summary.py "$EXEC_FILE" >> "$GITHUB_STEP_SUMMARY" - else - echo "## Claude Triage Run" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No execution output file found. Run may have been skipped or failed before writing output." >> "$GITHUB_STEP_SUMMARY" - fi From 02b2df3b7b1403f29facd1708e09d34fea4bce0c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:03:41 +0200 Subject: [PATCH 46/73] chore(skills): Drop bundler-plugins cross-repo search from triage-issue skill (#25099) The bundler plugins now live in this repo (`packages/bundler-plugins`), so the `triage-issue` skill no longer needs to search `getsentry/sentry-javascript-bundler-plugins`. Removes that search and the matching line in the report template, and points to the local package instead. via **Charly Gomez**. -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC0C55JYA84F%3A1790779997.226189) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0C55JYA84F%3A1790779997.226189/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Charly Gomez --- .agents/skills/triage-issue/SKILL.md | 3 +-- .agents/skills/triage-issue/assets/triage-report.md | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.agents/skills/triage-issue/SKILL.md b/.agents/skills/triage-issue/SKILL.md index c997734d1c99..6505c5b0d9dd 100644 --- a/.agents/skills/triage-issue/SKILL.md +++ b/.agents/skills/triage-issue/SKILL.md @@ -73,11 +73,10 @@ If any of these alternative interpretations apply, capture them in the triage re ### Step 3: Codebase Research -Search for relevant code using Grep/Glob. Find error messages, function names, and stack trace paths in the local repo. +Search for relevant code using Grep/Glob. Find error messages, function names, and stack trace paths in the local repo. Bundler plugin code lives in `packages/bundler-plugins`. Cross-repo searches (only when clearly relevant): -- Bundler issues: `gh api search/code -X GET -f "q=+repo:getsentry/sentry-javascript-bundler-plugins"` - Docs issues: `gh api search/code -X GET -f "q=+repo:getsentry/sentry-docs"` **Shell safety:** Strip shell metacharacters from issue-derived search terms before use in commands. diff --git a/.agents/skills/triage-issue/assets/triage-report.md b/.agents/skills/triage-issue/assets/triage-report.md index 3868d342348d..5b64cbda39b1 100644 --- a/.agents/skills/triage-issue/assets/triage-report.md +++ b/.agents/skills/triage-issue/assets/triage-report.md @@ -29,7 +29,6 @@ ### Cross-Repo Findings -- **bundler-plugins:** - **sentry-docs:** ### Recommended Next Steps From c25a4dd4dec968d51d6e7890efd48ce2c3bde1b9 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:08:56 +0200 Subject: [PATCH 47/73] chore(deps): Bump rrweb to 2.44.1 (#25100) Bumps `@sentry/rrweb` and `@sentry/rrweb-snapshot` from 2.43.2 to the newly released [2.44.1](https://github.com/getsentry/publish/issues/9776) in `@sentry-internal/replay`, `@sentry-internal/replay-canvas` and the browser integration tests. rrweb 2.44.1 also raises its `fflate` range to `^0.4.9`, so the lockfile picks up `fflate@0.4.9` (was 0.4.8). Changes: https://github.com/getsentry/rrweb/compare/c6d7f8e49a1fc10c374dfab6cf2cc5a8f5a982cd...refs/heads/release/2.44.1 via **Lukas Stracke**. -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC01C205FUAE%3A1791277328.518089) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC01C205FUAE%3A1791277328.518089/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Lukas Stracke --- .../browser-integration-tests/package.json | 2 +- packages/replay-canvas/package.json | 2 +- packages/replay-internal/package.json | 4 +- yarn.lock | 52 +++++++++---------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index a62b140e5dd4..d805439f943a 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -59,7 +59,7 @@ "@babel/core": "^7.29.6", "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.63.0", - "@sentry/rrweb": "2.43.2", + "@sentry/rrweb": "2.44.1", "@sentry/browser": "11.4.0", "@sentry/replay": "11.4.0", "@sentry/opentelemetry": "11.4.0", diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 00771c363692..256fadb80c7c 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -58,7 +58,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@sentry/rrweb": "2.43.2" + "@sentry/rrweb": "2.44.1" }, "dependencies": { "@sentry/replay": "11.4.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 2f67135e6161..340240b91bdf 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -73,8 +73,8 @@ "devDependencies": { "@babel/core": "^7.29.6", "@sentry-internal/replay-worker": "11.4.0", - "@sentry/rrweb": "2.43.2", - "@sentry/rrweb-snapshot": "2.43.2", + "@sentry/rrweb": "2.44.1", + "@sentry/rrweb-snapshot": "2.44.1", "fflate": "0.8.3", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.3.0" diff --git a/yarn.lock b/yarn.lock index 0c17c12ada19..5a6eec25bab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8009,38 +8009,38 @@ detect-libc "^2.0.4" node-abi "^3.89.0" -"@sentry/rrdom@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrdom/-/rrdom-2.43.2.tgz#24e20c5f9ffb085af9527fb5dbbcf30536322e8b" - integrity sha512-Tvb7TEGvGSmNEufxbMh1vyYq9HFC6kGhsTtdHJh6uBK1YEi09ybNqx7lPLk5PmJWsEsvdFu/Ef2Io+FSyXPq7Q== +"@sentry/rrdom@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrdom/-/rrdom-2.44.1.tgz#b7d9281ecc14a90e6dc0fc49bf75df31d7b41319" + integrity sha512-1w4FmGe1ddSOl7Qsu30f+SI2YdQSyEJ5Oo2zR8ymzCRJ0gaC8ZpkusGQe6tSLX3IwKL9DNLrEM4zO031+9BLtA== dependencies: - "@sentry/rrweb-snapshot" "2.43.2" + "@sentry/rrweb-snapshot" "2.44.1" -"@sentry/rrweb-snapshot@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrweb-snapshot/-/rrweb-snapshot-2.43.2.tgz#d0869a70d287f6adb20bc2838eb0e8a8229db39e" - integrity sha512-O8sC8pxiSo4QIEcPSYKyvNHBenlkNvitGUrku1yesVUhLAlF00WBf/9iHjB5EeAGFdVv9st3ObJpiF5q/+AKVw== +"@sentry/rrweb-snapshot@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrweb-snapshot/-/rrweb-snapshot-2.44.1.tgz#d1ca7d1af8e7cc4be7fbbb973c0b3a1f02bb0181" + integrity sha512-pMrOrncMfbN2gLe8IIGK6ReMThbNeCcnAxVff1EolxGHW6ry8qhVgFYhURZpDZuWLjiDOVAEcCFz/j3XTzMBGQ== -"@sentry/rrweb-types@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrweb-types/-/rrweb-types-2.43.2.tgz#e5de7070377f9da612204c1b633c93ea4505ef4e" - integrity sha512-xp5ogZmC+YnodPj/F9GqWRG+B1MiwfphNXjLYNE7nCr7lZpP1Iaoom7ctmu75SOP6UkR3YSS4jCW91EJ3UEjiw== +"@sentry/rrweb-types@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrweb-types/-/rrweb-types-2.44.1.tgz#9b79811dc22850f484f327764a26ed4e554dfc07" + integrity sha512-qsDPN5pfR1lswCrEpGKoQp5MNC6bpta/Fi/WpS41vpV6hCI7IWB9GYzD4PDgfERslGihY3S35v+wnanktsFgaw== dependencies: - "@sentry/rrweb-snapshot" "2.43.2" + "@sentry/rrweb-snapshot" "2.44.1" "@types/css-font-loading-module" "0.0.7" -"@sentry/rrweb@2.43.2": - version "2.43.2" - resolved "https://registry.yarnpkg.com/@sentry/rrweb/-/rrweb-2.43.2.tgz#cb92f3a30888834a2fb4ae84b93475814efa7050" - integrity sha512-/xJWPOO0zQoQe7+tUshv0OHCA0aGLcaQsS/qWsxsnmJzI9zOYfFHp22/xakmzaY6XLnAhCITe0wcI4HljItoVA== +"@sentry/rrweb@2.44.1": + version "2.44.1" + resolved "https://registry.yarnpkg.com/@sentry/rrweb/-/rrweb-2.44.1.tgz#8c7a1ba0606e81c5892779f976c336d5a6073221" + integrity sha512-DsxmVOlrsvSUgdbYELTtgP9zSan96NuZBM+kx85itij+/0ADBe6i2Mx2MLKQCMExenhnoZhTzf0ksZY0fkEymg== dependencies: - "@sentry/rrdom" "2.43.2" - "@sentry/rrweb-snapshot" "2.43.2" - "@sentry/rrweb-types" "2.43.2" + "@sentry/rrdom" "2.44.1" + "@sentry/rrweb-snapshot" "2.44.1" + "@sentry/rrweb-types" "2.44.1" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" - fflate "^0.4.4" + fflate "^0.4.9" mitt "^3.0.0" "@shikijs/core@1.29.2": @@ -15823,10 +15823,10 @@ fflate@0.8.3: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc" integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA== -fflate@^0.4.4: - version "0.4.8" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" - integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== +fflate@^0.4.9: + version "0.4.9" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.9.tgz#b5d1fd9776e4d3cbcd1a83a1f163b984a0972e21" + integrity sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw== figures@3.2.0, figures@^3.0.0: version "3.2.0" From 9ae9a3027daf12f31c7f848849dd1adbba732bc8 Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:17:56 +0200 Subject: [PATCH 48/73] test(nuxt): Upgrade to Nuxt 4.6 ; Test imports from `nuxt/server` (#25093) Nuxt 4.6 adds `nuxt/server`, a new import source for server code that is typed on the web-standard `RequestEvent` (nuxt/nuxt#36275). It is only an import path and the configured server builder supplies the implementation. By default that is Nitro with h3 v1. From the release notes: "Under the hood, `nuxt/server` is still powered by Nitro by default." This PR: - bumps the `nuxt-4` app to Nuxt 4.6 - runs the existing server tests a second time, against the routes that import only from `nuxt/server` - All of them pass with no SDK change This will make the review easy-peasy: image Closes https://github.com/getsentry/sentry-javascript/issues/25092 Related Nuxt release notes for 4.6: https://github.com/nuxt/nuxt/releases/tag/v4.6.0 --- .../nuxt-4/app/pages/fetch-server-routes.vue | 10 +- .../nuxt-4/app/pages/test-param/[param].vue | 3 +- .../app/pages/test-param/user/[userId].vue | 3 +- .../test-applications/nuxt-4/package.json | 2 +- .../server/api/nuxt-server/middleware-test.ts | 9 + .../server/api/nuxt-server/nitro-fetch.ts | 7 + .../api/nuxt-server/param-error/[param].ts | 5 + .../server/api/nuxt-server/server-error.ts | 5 + .../api/nuxt-server/third-party-http-error.ts | 16 ++ .../server/api/nuxt-server/user/[userId].ts | 7 + .../nuxt-server/06.nuxt-server-first.ts | 5 + .../nuxt-server/07.nuxt-server-auth.ts | 9 + .../nuxt-4/tests/errors.server.test.ts | 173 +++++++------- .../nuxt-4/tests/importSurfaces.ts | 6 + .../nuxt-4/tests/middleware.test.ts | 214 +++++++++--------- .../nuxt-4/tests/tracing.server.test.ts | 57 ++--- .../nuxt-4/tests/tracing.test.ts | 166 +++++++------- 17 files changed, 394 insertions(+), 303 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue index 3547773a1af9..2768eb370f94 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/fetch-server-routes.vue @@ -7,17 +7,19 @@ diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue index 019404aaf460..fde203b1575c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/[param].vue @@ -3,9 +3,10 @@ import { useRoute, useFetch } from '#imports'; const route = useRoute(); const param = route.params.param; +const apiPrefix = route.query.apiPrefix ?? '/api'; const fetchError = async () => { - await useFetch(`/api/param-error/${param}`); + await useFetch(`${apiPrefix}/param-error/${param}`); }; const fetchData = async () => { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue index 41daf0460b05..39d629b25ad6 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/test-param/user/[userId].vue @@ -3,8 +3,9 @@ import { useFetch, useRoute } from '#imports'; const route = useRoute(); const userId = route.params.userId as string; +const apiPrefix = route.query.apiPrefix ?? '/api'; -const { data } = await useFetch(`/api/user/${userId}`, { +const { data } = await useFetch(`${apiPrefix}/user/${userId}`, { server: false, // Don't fetch during SSR, only client-side }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index ac1ea1939999..5f84fb085b6d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -25,7 +25,7 @@ "ioredis": "5.10.1", "mongoose": "^9.10.2", "mysql": "^2.18.1", - "nuxt": "^4.1.2" + "nuxt": "^4.6.0" }, "devDependencies": { "@playwright/test": "~1.63.0", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts new file mode 100644 index 000000000000..ab0b283df34b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/middleware-test.ts @@ -0,0 +1,9 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(event => { + return { + message: 'Server middleware test endpoint', + path: event.url.pathname, + method: event.req.method, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts new file mode 100644 index 000000000000..8f8f4c4ae4fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/nitro-fetch.ts @@ -0,0 +1,7 @@ +import { defineEventHandler } from 'nuxt/server'; + +// `$fetch` is a Nitro global, not a `nuxt/server` export, so the portable copy uses `fetch`. +export default defineEventHandler(async () => { + const response = await fetch('https://example.com'); + return response.text(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts new file mode 100644 index 000000000000..6ad843e18b9f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/param-error/[param].ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(() => { + throw new Error('Nuxt 4 Param Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts new file mode 100644 index 000000000000..106f0ea9110c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/server-error.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(() => { + throw new Error('Nuxt 4 Server error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts new file mode 100644 index 000000000000..7b70b0af0586 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/third-party-http-error.ts @@ -0,0 +1,16 @@ +import { defineEventHandler } from 'nuxt/server'; + +// Mimics ky's and got's `HTTPError`: it shares its `name` with h3's error class, but keeps the +// status on `response` instead of on the error itself. +class ThirdPartyHTTPError extends Error { + public readonly response = { status: 404 }; + + public constructor(message: string) { + super(message); + this.name = 'HTTPError'; + } +} + +export default defineEventHandler(() => { + throw new ThirdPartyHTTPError('Nuxt 4 third-party HTTPError'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts new file mode 100644 index 000000000000..6fff4f8f91b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/nuxt-server/user/[userId].ts @@ -0,0 +1,7 @@ +import { defineEventHandler, getRouterParam } from 'nuxt/server'; + +export default defineEventHandler(event => { + const userId = getRouterParam(event, 'userId'); + + return `UserId Param: ${userId}!`; +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts new file mode 100644 index 000000000000..58d68a494905 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/06.nuxt-server-first.ts @@ -0,0 +1,5 @@ +import { defineEventHandler } from 'nuxt/server'; + +export default defineEventHandler(event => { + event.res.headers.set('x-nuxt-server-first-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts new file mode 100644 index 000000000000..62b19ad9c1ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/middleware/nuxt-server/07.nuxt-server-auth.ts @@ -0,0 +1,9 @@ +import { defineEventHandler, getQuery } from 'nuxt/server'; + +export default defineEventHandler(event => { + if (getQuery(event).throwNuxtServerError === 'true') { + throw new Error('nuxt/server auth middleware error'); + } + + event.res.headers.set('x-nuxt-server-auth-middleware', 'executed'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts index ea9c78b2d60e..418c09f16c57 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts @@ -1,94 +1,97 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; - -test.describe('server-side errors', async () => { - test('captures api fetch error (fetched on click)', async ({ page }) => { - const errorPromise = waitForError('nuxt-4', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Server API Error', { exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/server-error'); - - const exception0 = error.exception.values[0]; - const exception1 = error.exception.values[1]; - - expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Server error'); - expect(exception0.mechanism).toEqual({ - handled: true, - type: 'chained', - exception_id: 1, - parent_id: 0, - source: 'cause', - }); - - expect(exception1.type).toEqual('Error'); - expect(exception1.value).toEqual('Nuxt 4 Server error'); - expect(exception1.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', - exception_id: 0, - }); - }); - - test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { - const errorPromise = waitForError('nuxt-4', async errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; +import { IMPORT_SURFACES } from './importSurfaces'; + +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test.describe(`server-side errors (${name})`, () => { + test('captures api fetch error (fetched on click)', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Server error'; + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Server API Error', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/server-error`); + + const exception0 = error.exception.values[0]; + const exception1 = error.exception.values[1]; + + expect(exception0.type).toEqual('Error'); + expect(exception0.value).toEqual('Nuxt 4 Server error'); + expect(exception0.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(exception1.type).toEqual('Error'); + expect(exception1.value).toEqual('Nuxt 4 Server error'); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); - await page.goto(`/test-param/1234`); - await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/param-error/1234'); - - const exception0 = error.exception.values[0]; - const exception1 = error.exception.values[1]; - - expect(exception0.type).toEqual('Error'); - expect(exception0.value).toEqual('Nuxt 4 Param Server error'); - expect(exception0.mechanism).toEqual({ - handled: true, - type: 'chained', - exception_id: 1, - parent_id: 0, - source: 'cause', + test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 4 Param Server error'; + }); + + await page.goto(`/test-param/1234?apiPrefix=${apiPrefix}`); + await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/param-error/1234`); + + const exception0 = error.exception.values[0]; + const exception1 = error.exception.values[1]; + + expect(exception0.type).toEqual('Error'); + expect(exception0.value).toEqual('Nuxt 4 Param Server error'); + expect(exception0.mechanism).toEqual({ + handled: true, + type: 'chained', + exception_id: 1, + parent_id: 0, + source: 'cause', + }); + + expect(exception1.type).toEqual('Error'); + expect(exception1.value).toEqual('Nuxt 4 Param Server error'); + expect(exception1.mechanism).toEqual({ + handled: false, + type: 'auto.function.nuxt.nitro', + exception_id: 0, + }); }); - expect(exception1.type).toEqual('Error'); - expect(exception1.value).toEqual('Nuxt 4 Param Server error'); - expect(exception1.mechanism).toEqual({ - handled: false, - type: 'auto.function.nuxt.nitro', - exception_id: 0, + // ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the + // hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike + // is covered by the unit tests. + test('captures a thrown third-party `HTTPError`', async ({ page }) => { + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 4 third-party HTTPError'); + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click(); + + const error = await errorPromise; + + expect(error.transaction).toEqual(`GET ${apiPrefix}/third-party-http-error`); + expect(error.exception.values).toContainEqual( + expect.objectContaining({ + value: 'Nuxt 4 third-party HTTPError', + mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }), + }), + ); }); }); - - // ky and got name their errors `HTTPError` too. h3 wraps a thrown one in its own error before the - // hook sees it, so this checks it still gets reported. The hook's handling of an unwrapped lookalike - // is covered by the unit tests. - test('captures a thrown third-party `HTTPError`', async ({ page }) => { - const errorPromise = waitForError('nuxt-4', async errorEvent => { - return !!errorEvent?.exception?.values?.some(value => value.value === 'Nuxt 4 third-party HTTPError'); - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Third-Party HTTPError', { exact: true }).click(); - - const error = await errorPromise; - - expect(error.transaction).toEqual('GET /api/third-party-http-error'); - expect(error.exception.values).toContainEqual( - expect.objectContaining({ - value: 'Nuxt 4 third-party HTTPError', - mechanism: expect.objectContaining({ handled: false, type: 'auto.function.nuxt.nitro' }), - }), - ); - }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts new file mode 100644 index 000000000000..d0d4fd9a22d3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/importSurfaces.ts @@ -0,0 +1,6 @@ +// The server test routes under `apiPrefix` are copies of each other that differ only in where they import from. +// Reference: https://github.com/nuxt/nuxt/pull/36275 +export const IMPORT_SURFACES = [ + { name: '#imports', apiPrefix: '/api' }, + { name: 'nuxt/server', apiPrefix: '/api/nuxt-server' }, +] as const; diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts index fe44b684fe14..cc1dd5b14d2d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/middleware.test.ts @@ -1,139 +1,145 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpansUntilSegment, getSpanOp, waitForError } from '@sentry-internal/test-utils'; - -async function collectRequestSpans() { - const spans = await collectStreamedSpansUntilSegment( - 'nuxt-4', - span => span.attributes['url.path']?.value === '/api/middleware-test', - ); - const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === '/api/middleware-test'); +import { IMPORT_SURFACES } from './importSurfaces'; + +// Nitro runs every file in `server/middleware/` on every request, so each surface tells its spans apart by +// middleware name. `nuxt/server` has no object syntax (`onRequest`, `onBeforeResponse`) and no `eventHandler` +// alias, so only the simple middleware has a copy. +const MIDDLEWARE = { + '#imports': { + names: ['01.first', '02.second', '03.auth', '04.hooks', '05.array-hooks'], + simpleNames: ['01.first', '02.second', '03.auth'], + // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) + spanCount: 11, + authName: '03.auth', + authErrorQuery: 'throwError=true', + authErrorMessage: 'Auth middleware error', + }, + 'nuxt/server': { + names: ['06.nuxt-server-first', '07.nuxt-server-auth'], + simpleNames: ['06.nuxt-server-first', '07.nuxt-server-auth'], + spanCount: 2, + authName: '07.nuxt-server-auth', + authErrorQuery: 'throwNuxtServerError=true', + authErrorMessage: 'nuxt/server auth middleware error', + }, +} as const; + +async function collectRequestSpans(path = '/api/middleware-test') { + const spans = await collectStreamedSpansUntilSegment('nuxt-4', span => span.attributes['url.path']?.value === path); + const rootSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === path); return spans.filter(span => span.trace_id === rootSpan?.trace_id); } -test.describe('Server Middleware Instrumentation', () => { - test('should create separate spans for each server middleware', async ({ request }) => { - const spansPromise = collectRequestSpans(); +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + const middleware = MIDDLEWARE[name]; + const path = `${apiPrefix}/middleware-test`; + const isSurfaceMiddlewareSpan = (span: { attributes: Record }) => + (middleware.names as readonly unknown[]).includes(span.attributes['nuxt.middleware.name']?.value); - // Make request to the API endpoint that will trigger all server middleware - const response = await request.get('/api/middleware-test'); - expect(response.status()).toBe(200); + test.describe(`Server Middleware Instrumentation (${name})`, () => { + test('should create separate spans for each server middleware', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - const responseData = await response.json(); - expect(responseData.message).toBe('Server middleware test endpoint'); + const response = await request.get(path); + expect(response.status()).toBe(200); - const spans = await spansPromise; + const responseData = await response.json(); + expect(responseData.message).toBe('Server middleware test endpoint'); - // Verify that we have spans for each middleware - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); + const spans = await spansPromise; - // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) - expect(middlewareSpans).toHaveLength(11); - - // Check for specific middleware spans - const findSpanByName = (name: string) => - middlewareSpans.find(span => span.attributes['nuxt.middleware.name']?.value === name); - - const firstMiddlewareSpan = findSpanByName('01.first'); - const secondMiddlewareSpan = findSpanByName('02.second'); - const authMiddlewareSpan = findSpanByName('03.auth'); - const hooksOnRequestSpan = findSpanByName('04.hooks'); - const arrayHooksHandlerSpan = findSpanByName('05.array-hooks'); - - expect(firstMiddlewareSpan).toBeDefined(); - expect(secondMiddlewareSpan).toBeDefined(); - expect(authMiddlewareSpan).toBeDefined(); - expect(hooksOnRequestSpan).toBeDefined(); - expect(arrayHooksHandlerSpan).toBeDefined(); - - // Verify each span has the correct attributes - [firstMiddlewareSpan, secondMiddlewareSpan, authMiddlewareSpan].forEach(span => { - expect(span).toEqual( - expect.objectContaining({ - is_segment: false, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'middleware' }, - 'sentry.origin': { type: 'string', value: 'auto.middleware.nuxt' }, - 'http.request.method': { type: 'string', value: 'GET' }, - 'http.route': { type: 'string', value: '/api/middleware-test' }, - }), - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }), - ); - }); + const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware' && isSurfaceMiddlewareSpan(span)); - // Verify spans have different span IDs (each middleware gets its own span) - const spanIds = middlewareSpans.map(span => span.span_id); - const uniqueSpanIds = new Set(spanIds); - // 3 simple + 3 hooks (onRequest+handler+onBeforeResponse) + 5 array hooks (2 onRequest + 1 handler + 2 onBeforeResponse) - expect(uniqueSpanIds.size).toBe(11); + expect(middlewareSpans).toHaveLength(middleware.spanCount); - // Verify spans share the same trace ID - const traceIds = middlewareSpans.map(span => span.trace_id); - const uniqueTraceIds = new Set(traceIds); - expect(uniqueTraceIds.size).toBe(1); - }); + const findSpanByName = (middlewareName: string) => + middlewareSpans.find(span => span.attributes['nuxt.middleware.name']?.value === middlewareName); - test('middleware spans should have proper parent-child relationship', async ({ request }) => { - const spansPromise = collectRequestSpans(); + middleware.names.forEach(middlewareName => { + expect(findSpanByName(middlewareName)).toBeDefined(); + }); - await request.get('/api/middleware-test'); - const spans = await spansPromise; + middleware.simpleNames.forEach(middlewareName => { + expect(findSpanByName(middlewareName)).toEqual( + expect.objectContaining({ + is_segment: false, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'middleware' }, + 'sentry.origin': { type: 'string', value: 'auto.middleware.nuxt' }, + 'http.request.method': { type: 'string', value: 'GET' }, + 'http.route': { type: 'string', value: path }, + }), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }), + ); + }); - const segmentSpan = spans.find( - span => span.is_segment && span.attributes['url.path']?.value === '/api/middleware-test', - ); - const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware'); + // Verify spans have different span IDs (each middleware gets its own span) + const uniqueSpanIds = new Set(middlewareSpans.map(span => span.span_id)); + expect(uniqueSpanIds.size).toBe(middleware.spanCount); - // All middleware spans should be children of the request's segment span - middlewareSpans.forEach(span => { - expect(span.parent_span_id).toBe(segmentSpan?.span_id); + const uniqueTraceIds = new Set(middlewareSpans.map(span => span.trace_id)); + expect(uniqueTraceIds.size).toBe(1); }); - }); - test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { - const spansPromise = collectRequestSpans(); + test('middleware spans should have proper parent-child relationship', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - const errorEventPromise = waitForError('nuxt-4', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Auth middleware error'; + await request.get(path); + const spans = await spansPromise; + + const segmentSpan = spans.find(span => span.is_segment && span.attributes['url.path']?.value === path); + const middlewareSpans = spans.filter(span => getSpanOp(span) === 'middleware' && isSurfaceMiddlewareSpan(span)); + + expect(middlewareSpans).toHaveLength(middleware.spanCount); + middlewareSpans.forEach(span => { + expect(span.parent_span_id).toBe(segmentSpan?.span_id); + }); }); - // Make request with query param to trigger error in auth middleware - const response = await request.get('/api/middleware-test?throwError=true'); + test('should capture errors thrown in middleware and associate them with the span', async ({ request }) => { + const spansPromise = collectRequestSpans(path); - // The request should fail due to the middleware error - expect(response.status()).toBe(500); + const errorEventPromise = waitForError('nuxt-4', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === middleware.authErrorMessage; + }); - const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); + const response = await request.get(`${path}?${middleware.authErrorQuery}`); - // Find the auth middleware span - const authMiddlewareSpan = spans.find( - span => getSpanOp(span) === 'middleware' && span.attributes['nuxt.middleware.name']?.value === '03.auth', - ); + expect(response.status()).toBe(500); - expect(authMiddlewareSpan).toBeDefined(); + const [spans, errorEvent] = await Promise.all([spansPromise, errorEventPromise]); - // Verify the span has error status - expect(authMiddlewareSpan?.status).toBe('error'); + const authMiddlewareSpan = spans.find( + span => + getSpanOp(span) === 'middleware' && span.attributes['nuxt.middleware.name']?.value === middleware.authName, + ); + + expect(authMiddlewareSpan).toBeDefined(); + expect(authMiddlewareSpan?.status).toBe('error'); - // Verify the error event is associated with the correct request - expect(errorEvent.transaction).toContain('GET /api/middleware-test'); + expect(errorEvent.transaction).toContain(`GET ${path}`); - // Verify the error has the correct mechanism - expect(errorEvent.exception?.values?.[0]).toEqual( - expect.objectContaining({ - value: 'Auth middleware error', - type: 'Error', - mechanism: expect.objectContaining({ - handled: false, - type: 'auto.middleware.nuxt', + expect(errorEvent.exception?.values?.[0]).toEqual( + expect.objectContaining({ + value: middleware.authErrorMessage, + type: 'Error', + mechanism: expect.objectContaining({ + handled: false, + type: 'auto.middleware.nuxt', + }), }), - }), - ); + ); + }); }); +}); +// `nuxt/server` has no object syntax for middleware hooks, so these only run on the classic surface. +test.describe('Server Middleware Instrumentation (#imports hooks)', () => { test('should create spans for onRequest and onBeforeResponse hooks', async ({ request }) => { const spansPromise = collectRequestSpans(); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts index 16097e9d1dd2..4ef6d7c039e0 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.server.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; test('sends a server root span on pageload', async ({ page }) => { const serverSpanPromise = waitForStreamedSpan('nuxt-4', span => { @@ -38,34 +39,36 @@ test('does not send spans for build asset folder "_nuxt"', async ({ page }) => { expect(serverSpan.attributes['sentry.segment.name.source']?.value).toBe('route'); }); -test('captures server API calls made with Nitro $fetch', async ({ page }) => { - // The server-side http.client span can flush before its segment. Accumulate until both arrived. - const spansPromise = collectStreamedSpans('nuxt-4', spans => { - return ( - spans.some(span => span.is_segment && span.attributes['url.path']?.value === '/api/nitro-fetch') && - spans.some( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes('example.com'), - ) +IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test(`captures server API calls made with Nitro $fetch (${name})`, async ({ page }) => { + // The server-side http.client span can flush before its segment. Accumulate until both arrived. + const spansPromise = collectStreamedSpans('nuxt-4', spans => { + return ( + spans.some(span => span.is_segment && span.attributes['url.path']?.value === `${apiPrefix}/nitro-fetch`) && + spans.some( + span => getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'https://example.com/', + ) + ); + }); + + await page.goto(`/fetch-server-routes?apiPrefix=${apiPrefix}`); + await page.getByText('Fetch Nitro $fetch', { exact: true }).click(); + + const spans = await spansPromise; + + const serverSegmentSpan = spans.find( + span => span.is_segment && span.attributes['url.path']?.value === `${apiPrefix}/nitro-fetch`, + ); + const httpClientSpan = spans.find( + span => getSpanOp(span) === 'http.client' && span.attributes['url.full']?.value === 'https://example.com/', ); - }); - - await page.goto(`/fetch-server-routes`); - await page.getByText('Fetch Nitro $fetch', { exact: true }).click(); - - const spans = await spansPromise; - - const serverSegmentSpan = spans.find( - span => span.is_segment && span.attributes['url.path']?.value === '/api/nitro-fetch', - ); - const httpClientSpan = spans.find( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes('example.com'), - ); - expect(serverSegmentSpan).toBeDefined(); - expect(getSpanOp(serverSegmentSpan!)).toBe('http.server'); + expect(serverSegmentSpan).toBeDefined(); + expect(getSpanOp(serverSegmentSpan!)).toBe('http.server'); - expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan?.name).toBe('GET example.com'); - expect(httpClientSpan?.trace_id).toBe(serverSegmentSpan?.trace_id); - expect(httpClientSpan?.parent_span_id).toBe(serverSegmentSpan?.span_id); + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan?.name).toBe('GET example.com'); + expect(httpClientSpan?.trace_id).toBe(serverSegmentSpan?.trace_id); + expect(httpClientSpan?.parent_span_id).toBe(serverSegmentSpan?.span_id); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts index 70419d18389e..7ee91e3eeb9c 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts @@ -1,9 +1,9 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { IMPORT_SURFACES } from './importSurfaces'; test.describe('distributed tracing', () => { const PARAM = 's0me-param'; - const API_PATH = `/api/user/${PARAM}`; test('capture a distributed pageload trace', async ({ page }) => { const clientSpanPromise = waitForStreamedSpan('nuxt-4', span => { @@ -62,87 +62,93 @@ test.describe('distributed tracing', () => { expect(serverSpan.trace_id).toBe(metaTraceId); }); - test('capture a distributed trace from a client-side API request with parametrized routes', async ({ page }) => { - // The `http.client` span ends after the pageload segment, so it can be flushed in a later - // envelope. Accumulate until both spans have arrived. - const clientSpansPromise = collectStreamedSpans('nuxt-4', spans => { - return ( - spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && - spans.some( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), - ) + IMPORT_SURFACES.forEach(({ name, apiPrefix }) => { + test(`capture a distributed trace from a client-side API request with parametrized routes (${name})`, async ({ + page, + }) => { + const API_PATH = `${apiPrefix}/user/${PARAM}`; + + // The `http.client` span ends after the pageload segment, so it can be flushed in a later + // envelope. Accumulate until both spans have arrived. + const clientSpansPromise = collectStreamedSpans('nuxt-4', spans => { + return ( + spans.some(span => span.name === '/test-param/user/:userId()' && span.is_segment) && + spans.some( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), + ) + ); + }); + const ssrSpanPromise = waitForStreamedSpan('nuxt-4', span => { + return span.is_segment && span.name.includes('GET /test-param/user'); + }); + const serverReqSpanPromise = waitForStreamedSpan('nuxt-4', span => { + return span.is_segment && span.name.includes(`GET ${apiPrefix}/user/`); + }); + + // Navigate to the page which will trigger an API call from the client-side + await page.goto(`/test-param/user/${PARAM}?apiPrefix=${apiPrefix}`); + + const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ + clientSpansPromise, + ssrSpanPromise, + serverReqSpanPromise, + ]); + + const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); + const httpClientSpan = clientSpans.find( + span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), ); - }); - const ssrSpanPromise = waitForStreamedSpan('nuxt-4', span => { - return span.is_segment && span.name.includes('GET /test-param/user'); - }); - const serverReqSpanPromise = waitForStreamedSpan('nuxt-4', span => { - return span.is_segment && span.name.includes('GET /api/user/'); - }); - // Navigate to the page which will trigger an API call from the client-side - await page.goto(`/test-param/user/${PARAM}`); - - const [clientSpans, ssrSpan, serverReqSpan] = await Promise.all([ - clientSpansPromise, - ssrSpanPromise, - serverReqSpanPromise, - ]); - - const pageloadSpan = clientSpans.find(span => span.name === '/test-param/user/:userId()' && span.is_segment); - const httpClientSpan = clientSpans.find( - span => getSpanOp(span) === 'http.client' && `${span.attributes['url.full']?.value}`.includes(API_PATH), - ); - - expect(pageloadSpan).toMatchObject({ - name: '/test-param/user/:userId()', - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'pageload' }, - 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); - - expect(httpClientSpan).toBeDefined(); - expect(httpClientSpan).toMatchObject({ - // A relative fetch has no domain of its own, so it resolves against the page origin. - name: 'GET localhost', - parent_span_id: pageloadSpan?.span_id, // pageload span is parent - attributes: expect.objectContaining({ - type: { type: 'string', value: 'fetch' }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: expect.stringContaining(API_PATH) }, - 'url.domain': { type: 'string', value: 'localhost' }, - }), + expect(pageloadSpan).toMatchObject({ + name: '/test-param/user/:userId()', + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.vue' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + }), + }); + + expect(httpClientSpan).toBeDefined(); + expect(httpClientSpan).toMatchObject({ + // A relative fetch has no domain of its own, so it resolves against the page origin. + name: 'GET localhost', + parent_span_id: pageloadSpan?.span_id, // pageload span is parent + attributes: expect.objectContaining({ + type: { type: 'string', value: 'fetch' }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: expect.stringContaining(API_PATH) }, + 'url.domain': { type: 'string', value: 'localhost' }, + }), + }); + + expect(ssrSpan).toMatchObject({ + name: 'GET /test-param/user/:userId()', // parametrized route + is_segment: true, + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + }), + }); + + expect(serverReqSpan).toMatchObject({ + name: `GET ${apiPrefix}/user/:userId`, // parametrized route + is_segment: true, + parent_span_id: httpClientSpan?.span_id, // http.client span is parent + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'http.server' }, + 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, + }), + }); + + // `collectStreamedSpans` already guarantees the pageload and http.client spans share a trace, + // so only the independently awaited server spans need the check. + expect(pageloadSpan?.trace_id).toBeDefined(); + expect(pageloadSpan?.trace_id).toBe(ssrSpan.trace_id); + expect(pageloadSpan?.trace_id).toBe(serverReqSpan.trace_id); }); - - expect(ssrSpan).toMatchObject({ - name: 'GET /test-param/user/:userId()', // parametrized route - is_segment: true, - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - 'sentry.segment.name.source': { type: 'string', value: 'route' }, - }), - }); - - expect(serverReqSpan).toMatchObject({ - name: 'GET /api/user/:userId', // parametrized route - is_segment: true, - parent_span_id: httpClientSpan?.span_id, // http.client span is parent - attributes: expect.objectContaining({ - 'sentry.op': { type: 'string', value: 'http.server' }, - 'sentry.origin': { type: 'string', value: 'auto.http.http_server' }, - }), - }); - - // `collectStreamedSpans` already guarantees the pageload and http.client spans share a trace, - // so only the independently awaited server spans need the check. - expect(pageloadSpan?.trace_id).toBeDefined(); - expect(pageloadSpan?.trace_id).toBe(ssrSpan.trace_id); - expect(pageloadSpan?.trace_id).toBe(serverReqSpan.trace_id); }); }); From 345064dae7016c2c48a15f6f1bcbdb87b00fd5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 6 Oct 2026 17:05:07 +0300 Subject: [PATCH 49/73] fix(nextjs): Skip `init` inside a request of `@sentry/cloudflare` (#24996) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next.js calls `register()` inside the first request. When the Worker is wrapped with `withSentry` from `@sentry/cloudflare`, the server and edge `init` installed the OpenTelemetry async context strategy on the storage of that running request, and every later request failed with `context.getValue is not a function` (#24603). On Workers, `init` now creates no client while a request of `withSentry` runs, because that client already handles the request. The SDK detects this from the installed strategy (AsyncLocalStorage, a store is set, `Cloudflare-Workers` user agent) instead of a new option, so existing setups need no change. The server `init` still adds its global event processors and the `turbopack` tag, which also apply to the events of the Cloudflare client. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- docs/repeated-init.md | 1 + .../nextjs/src/common/utils/responseEnd.ts | 19 ++++++- packages/nextjs/src/edge/index.ts | 39 +++++++++---- packages/nextjs/src/server/index.ts | 36 +++++++++--- packages/nextjs/test/edgeSdk.test.ts | 27 ++++++++- packages/nextjs/test/serverSdk.test.ts | 57 ++++++++++++++++++- 6 files changed, 156 insertions(+), 23 deletions(-) diff --git a/docs/repeated-init.md b/docs/repeated-init.md index aa12aaf5bd23..a8f6445f0ffc 100644 --- a/docs/repeated-init.md +++ b/docs/repeated-init.md @@ -42,6 +42,7 @@ When `init()` replaces a client, nothing closes the old one: | Nuxt server | Keeps the first client and returns it. Logs that a `--import` preload is no longer needed. | | Cloudflare (default) | Keeps the first client of the isolate and returns it, unless that client is closed or closing. `cacheClient: false` makes a new client on each call, with no warning. | | Next.js edge | Warns, then replaces the client. Returns `void`. | +| Next.js server and edge in a request of `withSentry` (Cloudflare) | Creates no client, because the client of `withSentry` handles the request. Returns `undefined`. | The shared warning lives in `warnIfClientIsActive()` in `packages/core/src/sdk.ts`. Core exports it as diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index 31e3fa698433..4ec79d9cabdf 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { debug, fill, flush, GLOBAL_OBJ, setHttpStatus } from '@sentry/core'; +import { debug, fill, flush, getAsyncContextStrategy, getMainCarrier, GLOBAL_OBJ, setHttpStatus } from '@sentry/core'; import { vercelWaitUntil } from '@sentry/core/server'; import type { ServerResponse } from 'http'; import { DEBUG_BUILD } from '../debug-build'; @@ -105,3 +105,20 @@ export function cloudflareWaitUntil(task: Promise): void { export function isCloudflareWaitUntilAvailable(): boolean { return typeof _getOpenNextCloudflareContext()?.waitUntil === 'function'; } + +/** + * Whether a request of `withSentry` from `@sentry/cloudflare` runs. A client of `init` would then replace its async + * context strategy while the request runs (#24603). + */ +export function isAsyncContextOwnedByCloudflare(): boolean { + const strategy = getAsyncContextStrategy(getMainCarrier()); + // The AsyncLocalStorage strategy of `@sentry/cloudflare` has no `withActiveSpan`. + const asyncLocalStorage = strategy.getTracingChannelBinding?.()?.asyncLocalStorage as + | { getStore(): unknown } + | undefined; + return ( + !strategy.withActiveSpan && + asyncLocalStorage?.getStore() !== undefined && + GLOBAL_OBJ.navigator?.userAgent === 'Cloudflare-Workers' + ); +} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index b9b53f9c7667..e582927fd6e8 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -3,6 +3,7 @@ /* eslint-disable import/export */ import { applySdkMetadata, + debug, getGlobalScope, getIsolationScope, getRootSpan, @@ -28,7 +29,12 @@ import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunn import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan'; import { getNormalizedRequestFromAttributes } from '../common/utils/getNormalizedRequestFromAttributes'; import { isBuild } from '../common/utils/isBuild'; -import { flushSafelyWithTimeout, isCloudflareWaitUntilAvailable, waitUntil } from '../common/utils/responseEnd'; +import { + flushSafelyWithTimeout, + isAsyncContextOwnedByCloudflare, + isCloudflareWaitUntilAvailable, + waitUntil, +} from '../common/utils/responseEnd'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; @@ -105,6 +111,26 @@ export function init(options: VercelEdgeOptions = {}): void { ]; opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans]; + // Before the Cloudflare check, so that the events of the client of `withSentry` get the tag too. + try { + // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js + if (process.turbopack) { + getGlobalScope().setTag('turbopack', true); + getGlobalScope().setAttribute('turbopack', true); + } + } catch { + // Noop + // The statement above can throw because process is not defined on the client + } + + if (isAsyncContextOwnedByCloudflare()) { + DEBUG_BUILD && + debug.log( + 'The client of `withSentry` handles this Worker, so `init` creates no client. Set the options in `withSentry`.', + ); + return; + } + // Use appropriate SDK metadata based on the runtime environment if (isRunningOnCloudflare) { applySdkMetadata(opts, 'nextjs', ['nextjs', 'cloudflare']); @@ -213,15 +239,4 @@ export function init(options: VercelEdgeOptions = {}): void { waitUntil(flushSafelyWithTimeout()); }); - - try { - // @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js - if (process.turbopack) { - getGlobalScope().setTag('turbopack', true); - getGlobalScope().setAttribute('turbopack', true); - } - } catch { - // Noop - // The statement above can throw because process is not defined on the client - } } diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 4b486c4133d9..382e1ca850f7 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -2,7 +2,7 @@ // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; -import type { EventProcessor } from '@sentry/core'; +import type { EventProcessor, Scope } from '@sentry/core'; import { _INTERNAL_getActiveClient, applySdkMetadata, @@ -19,7 +19,7 @@ import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolica import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; -import { isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd'; +import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd'; import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; @@ -182,8 +182,11 @@ export function init(options: NodeOptions): NodeClient | undefined { DEBUG_BUILD && debug.log('Initializing SDK...'); + // The client of `withSentry` from `@sentry/cloudflare` handles this request, so `init` creates none. The global + // parts below still apply. + const isOwnedByCloudflare = isAsyncContextOwnedByCloudflare(); const existingClient = _INTERNAL_getActiveClient(); - if (existingClient) { + if (existingClient && !isOwnedByCloudflare) { DEBUG_BUILD && debug.log('SDK already initialized'); return existingClient; } @@ -191,7 +194,7 @@ export function init(options: NodeOptions): NodeClient | undefined { // Use appropriate SDK metadata based on the runtime environment applySdkMetadata(opts, 'nextjs', ['nextjs', cloudflareConfig ? 'cloudflare' : 'node']); - const client = nodeInit(opts); + const client = isOwnedByCloudflare ? undefined : nodeInit(opts); client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in @@ -233,8 +236,18 @@ export function init(options: NodeOptions): NodeClient | undefined { client?.on('spanEnd', maybeCleanupQueueSpan); // On the client, not the global scope, so a later `init()` after - // `close()` does not stack another copy. - client?.addEventProcessor( + // `close()` does not stack another copy. In a request of `withSentry` on + // Workers, `init` creates no client, so the global scope gets them, which + // reaches every client of `withSentry`, unless an earlier `init` added them. + let eventProcessorTarget: NodeClient | Scope | undefined = client; + if (isOwnedByCloudflare) { + const globalScope = getGlobalScope(); + const isAdded = globalScope + .getScopeData() + .eventProcessors.some(processor => processor.id === 'DropReactControlFlowErrors'); + eventProcessorTarget = isAdded ? undefined : globalScope; + } + eventProcessorTarget?.addEventProcessor( Object.assign( ((event, hint) => { if (event.type !== undefined) { @@ -281,7 +294,7 @@ export function init(options: NodeOptions): NodeClient | undefined { }); if (process.env.NODE_ENV === 'development') { - client?.addEventProcessor(devErrorSymbolicationEventProcessor); + eventProcessorTarget?.addEventProcessor(devErrorSymbolicationEventProcessor); } try { @@ -295,7 +308,14 @@ export function init(options: NodeOptions): NodeClient | undefined { // The statement above can throw because process is not defined on the client } - DEBUG_BUILD && debug.log('SDK successfully initialized'); + if (isOwnedByCloudflare) { + DEBUG_BUILD && + debug.log( + 'The client of `withSentry` handles this Worker, so `init` creates no client. Set the options in `withSentry`.', + ); + } else { + DEBUG_BUILD && debug.log('SDK successfully initialized'); + } return client; } diff --git a/packages/nextjs/test/edgeSdk.test.ts b/packages/nextjs/test/edgeSdk.test.ts index 0ec8d196a9a3..2cbd0ef0070b 100644 --- a/packages/nextjs/test/edgeSdk.test.ts +++ b/packages/nextjs/test/edgeSdk.test.ts @@ -1,5 +1,6 @@ import type { Integration } from '@sentry/core'; -import { GLOBAL_OBJ } from '@sentry/core'; +import { getGlobalScope, getMainCarrier, GLOBAL_OBJ, withIsolationScope } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils'; import * as SentryVercelEdge from '@sentry/vercel-edge'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../src/common/span-attributes-with-logic-attached'; @@ -22,6 +23,11 @@ describe('Edge init()', () => { afterEach(() => { SentryVercelEdge.getCurrentScope().setClient(undefined); vi.clearAllMocks(); + vi.unstubAllGlobals(); + + getMainCarrier().__SENTRY__ = undefined; + + delete (process as { turbopack?: boolean }).turbopack; }); it('inits the Vercel Edge SDK', () => { @@ -53,6 +59,25 @@ describe('Edge init()', () => { ); }); + it('skips init on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => init({})); + + expect(vercelEdgeInit).not.toHaveBeenCalled(); + }); + + it('sets the `turbopack` tag on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + (process as { turbopack?: boolean }).turbopack = true; + + withIsolationScope(() => init({})); + + expect(getGlobalScope().getScopeData().tags.turbopack).toBe(true); + }); + describe('integrations', () => { // Options passed by `@sentry/nextjs`'s `init` to `@sentry/vercel-edge`'s `init` after modifying them type ModifiedInitOptions = { integrations?: Integration[]; defaultIntegrations: Integration[] }; diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index dfee68309be9..3ac99be0037f 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,5 +1,6 @@ import type { Integration } from '@sentry/core'; -import { GLOBAL_OBJ, getMainCarrier } from '@sentry/core'; +import { GLOBAL_OBJ, getMainCarrier, withIsolationScope } from '@sentry/core'; +import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils'; import { close, getCurrentScope, getGlobalScope } from '@sentry/node'; import * as SentryNode from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -18,10 +19,12 @@ function findIntegrationByName(integrations: Integration[] = [], name: string): describe('Server init()', () => { afterEach(() => { vi.clearAllMocks(); + vi.unstubAllGlobals(); getMainCarrier().__SENTRY__ = undefined; delete process.env.VERCEL; + delete (process as { turbopack?: boolean }).turbopack; }); it('inits the Node SDK', () => { @@ -91,6 +94,58 @@ describe('Server init()', () => { expect(second).toBe(first); }); + it('skips init on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => init({})); + + expect(nodeInit).not.toHaveBeenCalled(); + }); + + it('sets the `turbopack` tag on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + (process as { turbopack?: boolean }).turbopack = true; + + withIsolationScope(() => init({})); + + expect(getGlobalScope().getScopeData().tags.turbopack).toBe(true); + }); + + it('adds its event processors to the global scope once on Cloudflare Workers in a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => { + init({}); + init({}); + }); + + expect( + getGlobalScope() + .getScopeData() + .eventProcessors.filter(processor => processor.id === 'DropReactControlFlowErrors'), + ).toHaveLength(1); + }); + + it('inits on Cloudflare Workers outside of a request of `withSentry` from `@sentry/cloudflare`', () => { + vi.stubGlobal('navigator', { userAgent: 'Cloudflare-Workers' }); + setAsyncLocalStorageAsyncContextStrategy(); + + init({}); + + expect(nodeInit).toHaveBeenCalledTimes(1); + }); + + it('inits outside of Cloudflare Workers when an AsyncLocalStorage strategy is installed', () => { + setAsyncLocalStorageAsyncContextStrategy(); + + withIsolationScope(() => init({})); + + expect(nodeInit).toHaveBeenCalledTimes(1); + }); + // TODO: test `vercel` tag when running on Vercel // Can't just add the test and set env variables, since the value in `index.server.ts` // is resolved when importing. From 92d9b8a819da3c7a63a332c0204ed10d6618ac5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 6 Oct 2026 17:05:08 +0300 Subject: [PATCH 50/73] ref(nextjs): Move the server span hooks into `serverSpanHooks.ts` (#24997) Moves the client hooks and the `ignoreSpans` list of the server `init` into `addNextjsServerSpanHooks` and `NEXTJS_SERVER_IGNORE_SPANS`, next to `handleOnSpanStart`. `@sentry/nextjs/cloudflare` registers the same hooks on the client of `@sentry/cloudflare`. In their new place next to `handleOnSpanStart`, they need no import of the server `init` module, so the Worker does not load `@sentry/node` or the code that this module runs on load. No behavior change. This PR prepares for the next stacked PRs, as the `index.ts` is, as said, importing from `@sentry/node`, but we would only be interested in the smaller pieces which are now refactored --------- Co-authored-by: Claude Opus 5.5 --- packages/nextjs/src/server/index.ts | 74 ++--------------- ...andleOnSpanStart.ts => serverSpanHooks.ts} | 81 ++++++++++++++++++- 2 files changed, 83 insertions(+), 72 deletions(-) rename packages/nextjs/src/server/{handleOnSpanStart.ts => serverSpanHooks.ts} (58%) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 382e1ca850f7..3b5ac2d0bd72 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,14 +1,12 @@ // import/export got a false positive, and affects most of our index barrel files // can be removed once following issue is fixed: https://github.com/import-js/eslint-plugin-import/issues/703 /* eslint-disable import/export */ -import { HTTP_TARGET, URL_QUERY } from '@sentry/conventions/attributes'; import type { EventProcessor, Scope } from '@sentry/core'; import { _INTERNAL_getActiveClient, applySdkMetadata, debug, getGlobalScope, - getRootSpan, getVercelEnv, GLOBAL_OBJ, } from '@sentry/core'; @@ -17,20 +15,12 @@ import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sent import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { isPrerenderControlFlowError } from '../common/nextNavigationErrorUtils'; -import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; import { isAsyncContextOwnedByCloudflare, isCloudflareWaitUntilAvailable } from '../common/utils/responseEnd'; -import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; -import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; -import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus'; -import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter'; -import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan'; -import { handleOnSpanStart } from './handleOnSpanStart'; +import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from './serverSpanHooks'; import { prepareSafeIdGeneratorContext } from './prepareSafeIdGeneratorContext'; import { nextjsUseCacheIntegration } from './useCacheInstrumentation'; -import { maybeCompleteCronCheckIn } from './vercelCronsMonitoring'; -import { maybeCleanupQueueSpan } from './vercelQueuesMonitoring'; export * from '@sentry/node'; @@ -159,22 +149,7 @@ export function init(options: NodeOptions): NodeClient | undefined { ...cloudflareConfig, }; - const nextjsIgnoreSpans: NonNullable = [ - // Static assets (matches `_next/static` anywhere in the name to handle custom basePath) - /^GET (\/.*)?\/_next\/static\//, - // Dev source-map fetch endpoints - /\/__nextjs_original-stack-frame/, - // Pages router /404 - /^\/404$/, - // App router /404 and /_not-found segments (any HTTP method) - /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/, - // Root transactions named "NextServer.getRequestHandler" containing useless tracing - /^NextServer\.getRequestHandler$/, - // Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION - // (set in `dropMiddlewareTunnelRequests` during `spanStart`) - { attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } }, - ]; - opts.ignoreSpans = [...(opts.ignoreSpans || []), ...nextjsIgnoreSpans]; + opts.ignoreSpans = [...(opts.ignoreSpans || []), ...NEXTJS_SERVER_IGNORE_SPANS]; if (DEBUG_BUILD && opts.debug) { debug.enable(); @@ -196,44 +171,9 @@ export function init(options: NodeOptions): NodeClient | undefined { const client = isOwnedByCloudflare ? undefined : nodeInit(opts); - client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { - // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in - // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing - // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. - // We need to drop these spans. - if ( - // eslint-disable-next-line typescript/no-deprecated - (typeof spanAttributes[HTTP_TARGET] === 'string' && - // eslint-disable-next-line typescript/no-deprecated - spanAttributes[HTTP_TARGET].includes('sentry_key') && - // eslint-disable-next-line typescript/no-deprecated - spanAttributes[HTTP_TARGET].includes('sentry_client')) || - (typeof spanAttributes[URL_QUERY] === 'string' && - spanAttributes[URL_QUERY].includes('sentry_key') && - spanAttributes[URL_QUERY].includes('sentry_client')) - ) { - samplingDecision.decision = false; - } - }); - - client?.on('spanStart', span => handleOnSpanStart(span, client)); - - // Normalize name/op/source/status on the request root span at span end, before it is serialized into - // a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles - // pick up the changes from one place, and the cron/queue hooks below see the finalized status. - client?.on('spanEnd', span => { - if (span !== getRootSpan(span)) { - return; - } - - const mutableRootSpan = createLiveRootSpanAdapter(span); - enhanceHandleRequestRootSpan(mutableRootSpan); - enhanceMiddlewareRootSpan(mutableRootSpan); - backfillHttpServerStatus(span); - }); - - client?.on('spanEnd', maybeCompleteCronCheckIn); - client?.on('spanEnd', maybeCleanupQueueSpan); + if (client) { + addNextjsServerSpanHooks(client); + } // On the client, not the global scope, so a later `init()` after // `close()` does not stack another copy. In a request of `withSentry` on @@ -289,10 +229,6 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - client?.on('preprocessEvent', event => { - setUrlProcessingMetadata(event); - }); - if (process.env.NODE_ENV === 'development') { eventProcessorTarget?.addEventProcessor(devErrorSymbolicationEventProcessor); } diff --git a/packages/nextjs/src/server/handleOnSpanStart.ts b/packages/nextjs/src/server/serverSpanHooks.ts similarity index 58% rename from packages/nextjs/src/server/handleOnSpanStart.ts rename to packages/nextjs/src/server/serverSpanHooks.ts index e2e3bb933d6f..fd6eb7fb2154 100644 --- a/packages/nextjs/src/server/handleOnSpanStart.ts +++ b/packages/nextjs/src/server/serverSpanHooks.ts @@ -3,9 +3,11 @@ import { HTTP_METHOD, HTTP_REQUEST_METHOD, HTTP_ROUTE, + HTTP_TARGET, SENTRY_ORIGIN, + URL_QUERY, } from '@sentry/conventions/attributes'; -import type { Client, Span } from '@sentry/core'; +import type { Client, Options, Span } from '@sentry/core'; import { getIsolationScope, getRootSpan, @@ -13,14 +15,87 @@ import { HTTP_SPAN_NAME_FALLBACK, spanToJSON, } from '@sentry/core'; +import { enhanceMiddlewareRootSpan } from '../common/enhanceMiddlewareRootSpan'; import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached'; import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes'; +import { backfillHttpServerStatus } from '../common/utils/backfillHttpServerStatus'; import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests'; import { createLiveRootSpanAdapter } from '../common/utils/liveRootSpanAdapter'; import { maybeForkIsolationScopeForRootSpan } from '../common/utils/forkIsolationScopeForRootSpan'; +import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata'; import { maybeEnhanceServerComponentSpanName } from '../common/utils/tracingUtils'; -import { maybeStartCronCheckIn } from './vercelCronsMonitoring'; -import { maybeEnrichQueueConsumerSpan, maybeEnrichQueueProducerSpan } from './vercelQueuesMonitoring'; +import { enhanceHandleRequestRootSpan } from './enhanceHandleRequestRootSpan'; +import { maybeCompleteCronCheckIn, maybeStartCronCheckIn } from './vercelCronsMonitoring'; +import { + maybeCleanupQueueSpan, + maybeEnrichQueueConsumerSpan, + maybeEnrichQueueProducerSpan, +} from './vercelQueuesMonitoring'; + +export const NEXTJS_SERVER_IGNORE_SPANS: NonNullable = [ + // Static assets (matches `_next/static` anywhere in the name to handle custom basePath) + /^GET (\/.*)?\/_next\/static\//, + // Dev source-map fetch endpoints + /\/__nextjs_original-stack-frame/, + // Pages router /404 + /^\/404$/, + // App router /404 and /_not-found segments (any HTTP method) + /^(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) \/(404|_not-found)$/, + // Root transactions named "NextServer.getRequestHandler" containing useless tracing + /^NextServer\.getRequestHandler$/, + // Spans flagged via TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION + // (set in `dropMiddlewareTunnelRequests` during `spanStart`) + { attributes: { [TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]: true } }, +]; + +/** + * Registers the hooks that name, enrich and filter the spans and events of the Next.js server on `client`. + */ +export function addNextjsServerSpanHooks(client: Client): void { + client.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { + // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in + // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing + // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. + // We need to drop these spans. + if ( + // eslint-disable-next-line typescript/no-deprecated + (typeof spanAttributes[HTTP_TARGET] === 'string' && + // eslint-disable-next-line typescript/no-deprecated + spanAttributes[HTTP_TARGET].includes('sentry_key') && + // eslint-disable-next-line typescript/no-deprecated + spanAttributes[HTTP_TARGET].includes('sentry_client')) || + (typeof spanAttributes[URL_QUERY] === 'string' && + spanAttributes[URL_QUERY].includes('sentry_key') && + spanAttributes[URL_QUERY].includes('sentry_client')) + ) { + samplingDecision.decision = false; + } + }); + + client.on('spanStart', span => handleOnSpanStart(span, client)); + + // Normalize name/op/source/status on the request root span at span end, before it is serialized into + // a transaction event (legacy) or streamed span JSON. Running on the live span means both lifecycles + // pick up the changes from one place, and the cron/queue hooks below see the finalized status. + client.on('spanEnd', span => { + if (span !== getRootSpan(span)) { + return; + } + + const mutableRootSpan = createLiveRootSpanAdapter(span); + enhanceHandleRequestRootSpan(mutableRootSpan); + enhanceMiddlewareRootSpan(mutableRootSpan); + backfillHttpServerStatus(span); + }); + + client.on('spanEnd', maybeCompleteCronCheckIn); + client.on('spanEnd', maybeCleanupQueueSpan); + + client.on('preprocessEvent', event => { + setUrlProcessingMetadata(event); + }); +} /** * Handles the on span start event for Next.js spans. From fcf0619db31aaf72f1e72683809724f9e199279c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:30:00 +0200 Subject: [PATCH 51/73] test(e2e): Bump bundler plugins to 5.4.1 in e2e test apps (#25107) Bumps the minimum `@sentry/vite-plugin` / `@sentry/rollup-plugin` versions from `^5.3.0` to `^5.4.1` in the `browser-webworker-vite`, `debug-id-sourcemaps`, `hydrogen-react-router-7` and `remix-hydrogen` e2e test apps. 5.4.1 depends on `@sentry/bundler-plugins@^10.76.0`. via **Lukas Stracke**. -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC01C205FUAE%3A1791290886.127829) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC01C205FUAE%3A1791290886.127829/?project=4510944073809921) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Lukas Stracke --- .../test-applications/browser-webworker-vite/package.json | 2 +- .../test-applications/debug-id-sourcemaps/package.json | 2 +- .../test-applications/hydrogen-react-router-7/package.json | 2 +- .../e2e-tests/test-applications/remix-hydrogen/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json index 0a51fa94443b..0aa7157512c5 100644 --- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz", - "@sentry/vite-plugin": "^5.3.0" + "@sentry/vite-plugin": "^5.4.1" }, "volta": { "node": "20.19.2", diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index 660ab6ae4628..8a24a5a34fbc 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -15,7 +15,7 @@ "devDependencies": { "rollup": "^4.35.0", "vitest": "^4.1.11", - "@sentry/rollup-plugin": "^5.3.0" + "@sentry/rollup-plugin": "^5.4.1" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index 9c90734a57c9..7687007647b3 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -16,7 +16,7 @@ "dependencies": { "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "@sentry/vite-plugin": "^5.3.0", + "@sentry/vite-plugin": "^5.4.1", "@shopify/hydrogen": "2025.5.0", "@shopify/remix-oxygen": "^3.0.0", "graphql": "^16.10.0", diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json index bc9c31b5c10c..5e27e3c1046e 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -19,7 +19,7 @@ "@remix-run/cloudflare-pages": "^2.17.4", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz", - "@sentry/vite-plugin": "^5.3.0", + "@sentry/vite-plugin": "^5.4.1", "@shopify/hydrogen": "2025.4.0", "@shopify/remix-oxygen": "2.0.10", "graphql": "^16.6.0", From ea31682ee6548a361ffd508b1664322eef629b5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:58:43 +0200 Subject: [PATCH 52/73] feat(deps): Bump js-yaml from 3.15.1 to 3.15.2 (#24236) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.15.1 to 3.15.2.
Changelog

Sourced from js-yaml's changelog.

3.15.2 - 2026-08-26

Changed

  • [backport] Hard-limit merge sequence size to 100.

Security

  • [backport] Count empty mappings in merge sequences toward maxTotalMergeKeys to limit CPU usage, #797.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=3.15.1&new-version=3.15.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5a6eec25bab0..f5e1dc9a8207 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18237,17 +18237,17 @@ js-tokens@^10.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.15.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0" - integrity sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag== + version "3.15.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.2.tgz#3f83823ac6be17f570f23b2ecdef3777ff5ea364" + integrity sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w== dependencies: argparse "^1.0.7" esprima "^4.0.0" js-yaml@^4.1.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848" - integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ== + version "4.3.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.2.tgz#8e44fb14a2643c59726bb15787b5f1512cb3d3fb" + integrity sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== dependencies: argparse "^2.0.1" From b49fbcca616b4b9df042ab2f724f36727fed0ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 6 Oct 2026 17:59:03 +0300 Subject: [PATCH 53/73] test(e2e): Cover isolation, trace propagation and logs on all react-router-8-framework runtimes (#25041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the assertions that #24836 lists and `react-router-8-framework` did not have: per-request isolation, incoming trace continuation, trace propagation to an outgoing `fetch`, and logs. They pass on Node, Bun, Deno and Cloudflare without a runtime branch, so the audit's Bun findings for isolation, incoming trace and outgoing `fetch`, and its Deno `fetch` finding, no longer reproduce with `@sentry/bun` and `@sentry/deno`. With every runtime green, the Bun, Deno and Cloudflare variants move to the required `variants`. The isolation test holds the first of two concurrent requests until the second one has set its tag, so both scopes are in use at the same time without a timer. The `fetch` test finds its spans through the `sentry-trace` header that the target route received, because the server span of a resource route is not named after the route yet (`GET /{*splat}` on Node and Deno, `GET` on Bun and Cloudflare). Part of #24836 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- dev-packages/e2e-tests/README.md | 3 +- .../react-router-8-framework/app/routes.ts | 4 +++ .../app/routes/api/headers.ts | 8 +++++ .../app/routes/isolation.ts | 28 +++++++++++++++++ .../app/routes/logs.ts | 6 ++++ .../app/routes/performance/outgoing-fetch.ts | 6 ++++ .../react-router-8-framework/package.json | 4 +-- .../tests/isolation.server.test.ts | 13 ++++++++ .../tests/logs.server.test.ts | 22 +++++++++++++ .../performance/incoming-trace.server.test.ts | 23 ++++++++++++++ .../performance/outgoing-fetch.server.test.ts | 31 +++++++++++++++++++ 11 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index e6424f4fa55f..f4fd271d5c74 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -139,7 +139,8 @@ Apps like `cloudflare-workers-send-to-sentry` deploy the built app as a real Wor ## Runtime variants (Bun, Deno, Cloudflare) To test a framework on a runtime other than Node.js, add an `optionalVariants` entry to the existing test app instead of -creating a new app. `react-router-8-framework` is the reference setup. +creating a new app. When the variant is green, move it to `variants`, so that it is required. `react-router-8-framework` +is the reference setup. - **`runtime`**: set `"runtime": "bun"`, `"deno"` or `"cloudflare"` on the variant. For such a variant, CI installs Bun or Deno, and the runner and CI set the `RUNTIME` env var for the build and the assert command. Without `runtime`, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts index 1e2b1b1dfd10..4da99dc5b86c 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts @@ -3,6 +3,9 @@ import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes export default [ index('routes/home.tsx'), route('__sentry-flush', 'routes/sentry-flush.tsx'), + route('api/headers', 'routes/api/headers.ts'), + route('isolation/:id', 'routes/isolation.ts'), + route('logs', 'routes/logs.ts'), ...prefix('errors', [ route('client', 'routes/errors/client.tsx'), route('client/:client-param', 'routes/errors/client-param.tsx'), @@ -21,5 +24,6 @@ export default [ route('with-middleware', 'routes/performance/with-middleware.tsx'), route('redis', 'routes/performance/redis.tsx'), route('db-mysql', 'routes/performance/db-mysql.tsx'), + route('outgoing-fetch', 'routes/performance/outgoing-fetch.ts'), ]), ] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts new file mode 100644 index 000000000000..900a5f946174 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/api/headers.ts @@ -0,0 +1,8 @@ +import type { Route } from './+types/headers'; + +export function loader({ request }: Route.LoaderArgs) { + return Response.json({ + 'sentry-trace': request.headers.get('sentry-trace'), + baggage: request.headers.get('baggage'), + }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts new file mode 100644 index 000000000000..1a0783990068 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/isolation.ts @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/react-router'; +import type { Route } from './+types/isolation'; + +const waitingRequests = new Map void>(); + +// Holds the first of two requests until the second one has set its tag, so both requests use their scopes at the +// same time. +function waitForPartner(id: string, partner: string): Promise { + const releasePartner = waitingRequests.get(partner); + if (releasePartner) { + waitingRequests.delete(partner); + releasePartner(); + return Promise.resolve(); + } + return new Promise(resolve => waitingRequests.set(id, resolve)); +} + +export async function loader({ params, request }: Route.LoaderArgs) { + Sentry.setTag('isolation-id', params.id); + + const partner = new URL(request.url).searchParams.get('partner'); + if (partner) { + await waitForPartner(params.id, partner); + } + + Sentry.captureMessage(`isolation ${params.id}`); + return Response.json({ id: params.id }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts new file mode 100644 index 000000000000..d218fd4c649c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/logs.ts @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/react-router'; + +export function loader() { + Sentry.logger.info('react-router server log', { route: 'logs' }); + return Response.json({ logged: true }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts new file mode 100644 index 000000000000..0f4535bb4943 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/outgoing-fetch.ts @@ -0,0 +1,6 @@ +import type { Route } from './+types/outgoing-fetch'; + +export async function loader({ request }: Route.LoaderArgs) { + const response = await fetch(new URL('/api/headers', request.url)); + return Response.json(await response.json()); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json index 7f22e8ce10f1..4cf3a981b6be 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json @@ -73,9 +73,7 @@ { "build-command": "pnpm test:build-latest", "label": "react-router-8-framework (latest)" - } - ], - "optionalVariants": [ + }, { "runtime": "bun", "label": "react-router-8-framework (bun)" diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts new file mode 100644 index 000000000000..dc2c775a080b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/isolation.server.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { APP_NAME } from './constants'; + +test('keeps the scope data of concurrent requests apart', async ({ request }) => { + const eventAPromise = waitForError(APP_NAME, event => event.message === 'isolation a'); + const eventBPromise = waitForError(APP_NAME, event => event.message === 'isolation b'); + + await Promise.all([request.get('/isolation/a?partner=b'), request.get('/isolation/b?partner=a')]); + + expect((await eventAPromise).tags).toMatchObject({ 'isolation-id': 'a' }); + expect((await eventBPromise).tags).toMatchObject({ 'isolation-id': 'b' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts new file mode 100644 index 000000000000..59566b6e208f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/logs.server.test.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; +import { APP_NAME } from './constants'; + +test('sends a log from a server loader', async ({ request }) => { + const logEnvelopeItemPromise = waitForEnvelopeItem(APP_NAME, ([header, body]) => { + return ( + header.type === 'log' && + (body as { items: { body: string }[] }).items.some(log => log.body === 'react-router server log') + ); + }); + + await request.get('/logs'); + + const [, body] = await logEnvelopeItemPromise; + const log = (body as { items: { body: string }[] }).items.find(item => item.body === 'react-router server log'); + + expect(log).toMatchObject({ + level: 'info', + attributes: { route: { value: 'logs', type: 'string' } }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts new file mode 100644 index 000000000000..5cf9d9f0bcbd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/incoming-trace.server.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test('continues the trace of an incoming request', async ({ request }) => { + const traceId = '12312012123120121231201212312012'; + const parentSpanId = '1121201211212012'; + + const spanPromise = waitForStreamedSpan(APP_NAME, span => span.is_segment && span.trace_id === traceId); + + await request.get('/performance/server-loader', { + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: `sentry-trace_id=${traceId},sentry-environment=qa,sentry-public_key=username,sentry-sample_rate=1,sentry-sampled=true`, + }, + }); + + const span = await spanPromise; + + expect(span.name).toBe('GET /performance/server-loader'); + expect(getSpanOp(span)).toBe('http.server'); + expect(span.parent_span_id).toBe(parentSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts new file mode 100644 index 000000000000..b24f0576f71c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test('propagates the trace to an outgoing fetch', async ({ request }) => { + const streamedSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); + return false; + }); + + const response = await request.get('/performance/outgoing-fetch'); + const headers = (await response.json()) as { 'sentry-trace': string | null; baggage: string | null }; + + expect(headers['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-1$/); + const [traceId, fetchSpanId] = headers['sentry-trace']!.split('-'); + expect(headers.baggage).toContain(`sentry-trace_id=${traceId}`); + + // The header names the span of the outgoing fetch, which belongs to the trace of the incoming request. + await expect.poll(() => streamedSpans.find(span => span.span_id === fetchSpanId)).toBeDefined(); + const fetchSpan = streamedSpans.find(span => span.span_id === fetchSpanId)!; + expect(getSpanOp(fetchSpan)).toBe('http.client'); + expect(fetchSpan.trace_id).toBe(traceId); + + await expect + .poll(() => streamedSpans.find(span => span.is_segment && span.trace_id === traceId && !span.parent_span_id)) + .toBeDefined(); + const serverSpan = streamedSpans.find(span => span.is_segment && span.trace_id === traceId && !span.parent_span_id)!; + expect(getSpanOp(serverSpan)).toBe('http.server'); +}); From 2ea03002bc3ef53de486259d6eb1dd363cc65f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 6 Oct 2026 17:59:04 +0300 Subject: [PATCH 54/73] feat(react-router): Export createSentryServerInstrumentation from the Cloudflare entry (#25042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs set up React Router's instrumentation API with `export const instrumentations = [Sentry.createSentryServerInstrumentation()]`, but the Cloudflare entry (`@sentry/react-router/cloudflare`, and `@sentry/react-router` under the `workerd` condition) did not export the function, so a Worker could not follow that step. Its imports are runtime-independent, so the Cloudflare entry now exports it too. Without the API, the SDK names a request after its route only when React Router renders a document. Resource routes and errors in loaders and actions kept the name of the server below React Router: `GET /{*splat}` on Node and Deno, `GET` or none on Bun and Cloudflare. `react-router-8-framework` now exports `instrumentations` on every runtime, like the docs, so these are named after the route everywhere. The error tests lose their runtime branches and their todo, and the error mechanism and the `http.server` origin change to the instrumentation API's values. `react-router-7-framework` still covers the setup without the API. Part of #24836 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- .../app/entry.server.cloudflare.tsx | 2 ++ .../app/entry.server.tsx | 2 ++ .../tests/errors/errors.server.test.ts | 18 ++++-------------- .../performance/outgoing-fetch.server.test.ts | 1 + .../performance/performance.server.test.ts | 4 ++-- packages/react-router/src/cloudflare/index.ts | 4 ++++ 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx index e04fe621e718..31a4fec981f6 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.cloudflare.tsx @@ -48,3 +48,5 @@ export const handleError: HandleErrorFunction = (error, { request }) => { }; export default Sentry.wrapSentryHandleRequest(handleRequest); + +export const instrumentations = [Sentry.createSentryServerInstrumentation()]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx index 01682288ab71..9f9a70db740a 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.server.tsx @@ -18,3 +18,5 @@ const handleRequest = Sentry.createSentryHandleRequest({ export default handleRequest; export const handleError: HandleErrorFunction = Sentry.createSentryHandleError({ logErrors: true }); + +export const instrumentations = [Sentry.createSentryServerInstrumentation()]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts index bf436aa3c866..35eb7a8854de 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/errors/errors.server.test.ts @@ -21,17 +21,12 @@ test.describe('server-side errors', () => { value: errorMessage, mechanism: { handled: false, - type: 'react-router', + type: 'react_router.loader', }, }, ], }, - // Express names the transaction on Node and Deno. On Bun, where Express is not instrumented under - // `bun run`, it stays the request path. On Cloudflare the error has no transaction. - // todo: should be 'GET /errors/server-loader' everywhere - ...(RUNTIME === 'cloudflare' - ? {} - : { transaction: RUNTIME === 'bun' ? 'GET /errors/server-loader' : 'GET /{*splat}' }), + transaction: 'GET /errors/server-loader', request: { url: expect.stringContaining('errors/server-loader'), headers: expect.any(Object), @@ -74,17 +69,12 @@ test.describe('server-side errors', () => { value: errorMessage, mechanism: { handled: false, - type: 'react-router', + type: 'react_router.action', }, }, ], }, - // Express names the transaction on Node and Deno. On Bun, where Express is not instrumented under - // `bun run`, it stays the request path. On Cloudflare the error has no transaction. - // todo: should be 'POST /errors/server-action' everywhere - ...(RUNTIME === 'cloudflare' - ? {} - : { transaction: RUNTIME === 'bun' ? 'POST /errors/server-action.data' : 'POST /{*splat}' }), + transaction: 'POST /errors/server-action', request: { url: expect.stringContaining('errors/server-action'), headers: expect.any(Object), diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts index b24f0576f71c..dd8edc2d9da4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/outgoing-fetch.server.test.ts @@ -28,4 +28,5 @@ test('propagates the trace to an outgoing fetch', async ({ request }) => { .toBeDefined(); const serverSpan = streamedSpans.find(span => span.is_segment && span.trace_id === traceId && !span.parent_span_id)!; expect(getSpanOp(serverSpan)).toBe('http.server'); + expect(serverSpan.name).toBe('GET /performance/outgoing-fetch'); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts index 4488f1065229..c3f594ee2061 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts @@ -23,7 +23,7 @@ test.describe('server - performance', () => { expect(getSpanOp(span)).toBe('http.server'); expect(span.attributes).toMatchObject({ 'sentry.op': { value: 'http.server', type: 'string' }, - 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.instrumentation_api', type: 'string' }, 'sentry.segment.name.source': { value: 'route', type: 'string' }, 'sentry.environment': { value: 'qa', type: 'string' }, 'sentry.sdk.name': { @@ -56,7 +56,7 @@ test.describe('server - performance', () => { expect(getSpanOp(span)).toBe('http.server'); expect(span.attributes).toMatchObject({ 'sentry.op': { value: 'http.server', type: 'string' }, - 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.instrumentation_api', type: 'string' }, 'sentry.segment.name.source': { value: 'route', type: 'string' }, 'sentry.environment': { value: 'qa', type: 'string' }, 'sentry.sdk.name': { diff --git a/packages/react-router/src/cloudflare/index.ts b/packages/react-router/src/cloudflare/index.ts index 67db950b59fb..b94430728125 100644 --- a/packages/react-router/src/cloudflare/index.ts +++ b/packages/react-router/src/cloudflare/index.ts @@ -4,6 +4,10 @@ import { isPrerenderRequest } from '../server/serverBuild'; export * from '../client'; export { wrapSentryHandleRequest } from '../server/wrapSentryHandleRequest'; +export { + createSentryServerInstrumentation, + type CreateSentryServerInstrumentationOptions, +} from '../server/createServerInstrumentation'; /** * Injects Sentry trace meta tags into the HTML response by transforming the ReadableStream. From 18e4b7e626f8248930edbc086b36623c8e208eb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:00:02 +0200 Subject: [PATCH 55/73] feat(deps): bump compression from 1.8.1 to 1.8.2 (#25109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [compression](https://github.com/expressjs/compression) from 1.8.1 to 1.8.2.
Release notes

Sourced from compression's releases.

v1.8.2

Important

What's Changed

New Contributors

Full Changelog: https://github.com/expressjs/compression/compare/v1.8.1...v1.8.2

Changelog

Sourced from compression's changelog.

1.8.2

Commits
  • 0f97074 1.8.2 (#287)
  • 151f63e fix: destroy compression stream on response close
  • 0a76495 fix: match Cache-Control no-transform directive case-insensitively (#286)
  • c17b6e5 docs: update outdated Brotli note and fix npm install docs URL (#276)
  • 112911a chore(ci): npm-publish via reusable workflows (#269)
  • 1bf5eb0 build(deps): bump actions/upload-artifact from 5.0.0 to 6.0.0 (#267)
  • d8fe64d build(deps): bump actions/setup-node from 6.0.0 to 6.1.0 (#266)
  • b218ff5 build(deps): bump github/codeql-action from 4.31.5 to 4.31.9 (#265)
  • 8a1cf8e build(deps): bump actions/download-artifact from 6.0.0 to 7.0.0 (#268)
  • 4a19855 build(deps): bump ossf/scorecard-action from 2.4.2 to 2.4.3 (#257)
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for compression since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=compression&package-manager=npm_and_yarn&previous-version=1.8.1&new-version=1.8.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f5e1dc9a8207..93ffe9c4a3f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13005,13 +13005,14 @@ compressible@~2.0.18: mime-db ">= 1.43.0 < 2" compression@^1.7.4: - version "1.8.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" - integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + version "1.8.2" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.2.tgz#781397127ae2da00c0c5f7c76bda5b2a9b32a49f" + integrity sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ== dependencies: bytes "3.1.2" compressible "~2.0.18" debug "2.6.9" + destroy "1.2.0" negotiator "~0.6.4" on-headers "~1.1.0" safe-buffer "5.2.1" From 3cafe937afd096d88789fcc7089bc3b0e5c64c73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:07:18 +0200 Subject: [PATCH 56/73] feat(deps): bump shell-quote from 1.10.0 to 1.12.0 (#25108) Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.10.0 to 1.12.0.
Changelog

Sourced from shell-quote's changelog.

v1.12.0 - 2026-10-02

Fixed

Commits

  • [New] parse: support tab-stripping here-documents (&lt;&lt;-) dbfac37
  • [New] parse: support output process substitution (&gt;() 2053315
  • [New] parse: support the case test-next terminator (;;&) 7d688b9
  • [New] parse: support the case fall-through terminator (;&) f27010e
  • [New] parse: support redirecting output despite noclobber (&gt;|) b78d19c
  • [New] parse: support opening a file for reading and writing (&lt;&gt;) 21cc333
  • [New] parse: support redirecting stdout and stderr (&&gt;) 6ad6cd2
  • [New] parse: support appending stdout and stderr (&&gt;&gt;) 90cde9c
  • [Dev Deps] update @ljharb/eslint-config 3a7b4ae

v1.11.0 - 2026-09-29

Fixed

Commits

  • [Fix] quote: reject line terminators in tokens after a comment 6002b2e
  • [Fix] parse: preserve text after special shell parameters 81b08a5
  • [Fix] parse: an escaped backslash does not escape the character after it d708019
  • [Fix] quote: preserve ! in arguments that also contain ' ad39927
  • [Fix] parse: treat $_name as a variable name, not $_ followed by text 28f88cd
  • [Fix] quote: preserve empty glob patterns 35c9b97
  • [Fix] quote: escape ~ in glob patterns to prevent shell tilde-expansion 239d49c
  • [Dev Deps] update @ljharb/eslint-config, auto-changelog, eslint, evalmd b1e406e
  • [meta] npmignore some files ebfc308
  • [actions] add permissions 3429b0d
  • [actions] set least-privilege cache-mode 36f2394
  • [Dev Deps] update eslint 6de9a41
Commits
  • 6ecb8aa v1.12.0
  • 3a7b4ae [Dev Deps] update @ljharb/eslint-config
  • 7d688b9 [New] parse: support the case test-next terminator (;;&)
  • f27010e [New] parse: support the case fall-through terminator (;&)
  • 90cde9c [New] parse: support appending stdout and stderr (&>>)
  • 6ad6cd2 [New] parse: support redirecting stdout and stderr (&>)
  • 2053315 [New] parse: support output process substitution (>()
  • b78d19c [New] parse: support redirecting output despite noclobber (>|)
  • 21cc333 [New] parse: support opening a file for reading and writing (\<>)
  • dbfac37 [New] parse: support tab-stripping here-documents (<<-)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=shell-quote&package-manager=npm_and_yarn&previous-version=1.10.0&new-version=1.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 93ffe9c4a3f4..57794e2f0aea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25004,9 +25004,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.7.3, shell-quote@^1.8.4: - version "1.10.0" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.10.0.tgz#482033e192e4f5c07151521ffa03400ec71b1b0f" - integrity sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA== + version "1.12.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.12.0.tgz#e98fdfd2e921d8a58ae2820f4fc0f75c39f09675" + integrity sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q== shiki@^1.22.0, shiki@^1.23.1, shiki@^1.26.1: version "1.29.2" From 095f48573393c791fd548ab70dd3a513df671bc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:07:39 +0200 Subject: [PATCH 57/73] chore(deps-dev): bump nx from 22.7.7 to 22.7.10 (#25075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [nx](https://github.com/nrwl/nx/tree/HEAD/packages/nx) from 22.7.7 to 22.7.10.
Release notes

Sourced from nx's releases.

22.7.10 (2026-09-09)

🩹 Fixes

  • core: validate the migrations path before extracting package migrations (#36887)

❤️ Thank You

22.7.9 (2026-09-01)

🩹 Fixes

  • core: restrict daemon and plugin worker socket access to the owning user (#36370, #36586, #36463)
  • core: compare daemon workspace roots case-insensitively on Windows (#36835, #36722)
  • core: close daemon log descriptors after spawn to avoid Node 26 crash (#36280)
  • core: port quoteShellArg helper for no-shell spawn fixes (#36215, #36626, #35954)
  • maven: stop running maven and gradle graph analysis through a shell (#36626, #36215)

❤️ Thank You

22.7.8 (2026-07-30)

🩹 Fixes

  • angular-rspack: dispose stylesheet bundler so one-shot builds exit (#35869)
  • core: use workspace package manager when fetching migrations via install (#35866)
  • core: omit peer dependencies when installing packages to a temp dir (#36295)
  • core: prevent shell injection in nx import (#36348)
  • core: stop passing git revisions through a shell in affected commands (#36379)
  • core: allow nx build scripts in generated pnpm-workspace.yaml (#35564)
  • core: acknowledge @​swc/core build script in generated pnpm-workspace.yaml (#35608)
  • core: keep real dependencies when omitting peers from npm temp installs (#36518, #36295)
  • core: bump pinned axios and brace-expansion past vulnerable versions (#36507, #36474)
  • detox: resolve jest versions locally instead of importing @​nx/jest internals (3ce3f6ab6e)
  • docker: run release pipeline docker commands without a shell (#36505)
  • js: exclude typescript 7 from supported versions on 22.7.x (dc804964e9)
  • js: resolve the verdaccio bin through its package.json (#36479)
  • misc: use default import for chalk in @​nx/workspace output.ts (#35523, #35521, #34111, #21201, #26667)
  • repo: use default inputs instead of the '...' token in nx-dev on 22.7.x (#35530)
  • repo: trust wix/brew tap so macOS detox CI can install applesimutils (#36146)

... (truncated)

Commits
  • 38cd82a fix(core): validate the migrations path before extracting package migrations ...
  • 02c5d59 fix(maven): stop running maven and gradle graph analysis through a shell (#36...
  • 81698b9 fix(core): port quoteShellArg helper for no-shell spawn fixes
  • aa5dc40 fix(core): close daemon log descriptors after spawn to avoid Node 26 crash (#...
  • 7f9e528 fix(core): compare daemon workspace roots case-insensitively on Windows (#36835)
  • 71c2253 fix(core): restrict daemon and plugin worker socket access to the owning user...
  • f7afd77 fix(core): bump pinned axios and brace-expansion past vulnerable versions (#3...
  • 983413b fix(core): keep real dependencies when omitting peers from npm temp installs ...
  • 4159295 fix(core): stop passing git revisions through a shell in affected commands (#...
  • cf99649 fix(core): prevent shell injection in nx import (#36348)
  • Additional commits viewable in compare view

--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Charly Gomez Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- .github/dependency-review-config.yml | 14 +++ package.json | 2 +- yarn.lock | 153 ++++++++++++++------------- 3 files changed, 94 insertions(+), 75 deletions(-) diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 2cb41a456e17..5f82ea77210f 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -30,3 +30,17 @@ allow-ghsas: # 0.35.0, which is outside that range. Already present on develop via sharp 0.32.6 # and 0.34.5, both of which are affected by the same advisory. - GHSA-f88m-g3jw-g9cj + # axios, pinned exactly to 1.18.1 by nx (dev-only build tooling, not shipped). + # Can be removed once nx pins axios >= 1.20.0. + - GHSA-542g-h47m-68v8 + - GHSA-3pq3-5fj3-cg6v + - GHSA-x97p-jq2g-jp4f + - GHSA-mghh-pgcx-3jjj + - GHSA-c29m-xwm3-cm6r + - GHSA-r4gj-5m52-g5wh + - GHSA-m8m8-qj5v-23w3 + # brace-expansion, pinned exactly to 5.0.8 by nx (dev-only build tooling, not shipped). + # Can be removed once nx pins brace-expansion >= 5.0.11. + - GHSA-rgw5-rvv9-x895 + - GHSA-6j4f-fj2g-mc7p + - GHSA-qhr7-859c-m2p7 diff --git a/package.json b/package.json index f755d4692b92..4eb2f0af3612 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "madge": "8.0.0", "nodemon": "^3.1.10", "npm-run-all2": "^6.2.0", - "nx": "22.7.7", + "nx": "22.7.10", "oxfmt": "^0.70.0", "oxlint": "^1.75.0", "oxlint-tsgolint": "7", diff --git a/yarn.lock b/yarn.lock index 57794e2f0aea..10ded9708446 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6132,55 +6132,55 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@nx/nx-darwin-arm64@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.7.tgz#50576085d00470762e0fc40f51d1ba6fd56c6d24" - integrity sha512-HGu8Bc3DzmIFeKHzIEDBDScgI8/hOjVIj/EzJBE5289dUf+CQ5OSXc6kw2NL9SiCMAPKa0clqcJgLJLqP5t/DQ== - -"@nx/nx-darwin-x64@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.7.tgz#05255685f6b7e80d4b490a1e207c150413b275d0" - integrity sha512-J7z/Hh+bEKB9xvHdmtO5sK7YytFx8Ry6YpvYI2PoSl2/xDBca7q69fzHKTtm4S/8FvA1Qoi/m//JTIMQUB7cKA== - -"@nx/nx-freebsd-x64@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.7.tgz#0917685ae21512ddcdb26bd7f7107ffbd7964622" - integrity sha512-9H4bbI3fBr8Ct+6xmF6dqDKEvER60XV3tanXTSVtUBxyAVw3cPmRh7aMAu5PNyN+ewzRAJvV6KI4Rx1kA+s9jw== - -"@nx/nx-linux-arm-gnueabihf@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.7.tgz#6f925157c87f4bfcc83d62dac2c09ad1eec0c676" - integrity sha512-QsGaaZ7PI18c7rFgvUe0F0UYa5oErSKY6Yff6HVXy2zAAWluM4F/TXIRYbtXlvSq++CHhFXxk29Xb3+/zGR/rg== - -"@nx/nx-linux-arm64-gnu@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.7.tgz#4955e8b86b6dab0e7f3bf25647408ea2a5b4079f" - integrity sha512-CfzWaS+b32lI/inlYPv1ZAK9CWTWFHzT7TPThvOHML65nrgFl8cQ4Z4FeGdyCbHu2NoG3vR1E36O2tPI2/DLGg== - -"@nx/nx-linux-arm64-musl@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.7.tgz#c22c05e1822a3389e11b14967acfa92871634f3a" - integrity sha512-53QFuODMEHc1gobCT2WOmYz89DZtEIuLphirdIgnXkkPBTdrP77LPbJUXMRXCMKr90BQaSWhTX+J/ubANRE8og== - -"@nx/nx-linux-x64-gnu@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.7.tgz#d5887573e7c6c0f9a51010d75001b31607644bc6" - integrity sha512-unzmqGIDXGzujo1ZtbrMj2e5D5ldQ1feZmqDPhvO4casK2d96jpT8LtgIILpVDUfhLjl+By185gb0ArrWTsyKw== - -"@nx/nx-linux-x64-musl@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.7.tgz#2f1c357f5c479a0785178469955ffac231b8cd6f" - integrity sha512-oTjMGuM105ok8aH5rlg2YP0Kgkjg0VfUj1exwp49S70gGBJ0r8v5rEtJwOSdCf1WTHuEh0xi66Y/b1S0khF8dg== - -"@nx/nx-win32-arm64-msvc@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.7.tgz#4740d3151fa52dcb55eb4c1b3bbed88c78b9eb96" - integrity sha512-K741meG/l48TPPeDqnhYTV7pP+1ljjZ+0DRJBxMrPFIu8qKE3Abko/7NKWmWRjcSXJvtxvYkgG3wZds4N2Bhow== - -"@nx/nx-win32-x64-msvc@22.7.7": - version "22.7.7" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.7.tgz#87cc09dc269e3582065d8cdd78741f53af345f0e" - integrity sha512-AHVjXkreXjVKAKpMNCsF5nQC/et6lpQcERRm8AYOCncI59hTjYOLpdCMBLrIHA3hgc6SUYU8n5+tMAbv7zXXiQ== +"@nx/nx-darwin-arm64@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.10.tgz#f3ade8546f98faf83b05d92f0dedfa7f831658ab" + integrity sha512-gy0ASfsnFsMmTLQaGTQyf8l95kAgawBpdMrgtxsnXzyKAasqzfIEhEbb74/6+DmsmcnNyfBvAKvVvFBiNs3ZYw== + +"@nx/nx-darwin-x64@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.10.tgz#8c4129e946d15919ee93106c87658f7ee850b2d2" + integrity sha512-IGNSVU85ktrNm9cH9yLgSaQjmRVCcEldaZwtAqNaMcNU+Go5xY4zYv8tDQ2MJ3hXpi6pNdcFzZp8tZIJliHyPA== + +"@nx/nx-freebsd-x64@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.10.tgz#d2e6bf8adc3e156e0a5a98d3fe22aeb40e378a5d" + integrity sha512-KlmBmvD8uE7Saah3DO61D7ywn08FtYJE0I0AwZGs+0cisY1bUawtZZNYvbPTprHaCOWvbU2GeIVstK34vmbVXg== + +"@nx/nx-linux-arm-gnueabihf@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.10.tgz#e395493ffff3e1702b8b14b99d84e92842fb7539" + integrity sha512-xwCv9gR968hMeeWUqBneLR9nBqP+ECPVFumdWCxMBh116ftCjMMik8E3BdUKLdqreVYFJdwvJRPortHj/YYikA== + +"@nx/nx-linux-arm64-gnu@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.10.tgz#e55aceb9352e32eda3c1adfb8c89172a8b463000" + integrity sha512-AiwBgerLicRzcyEevqwDuP22Nj5nFFrjJsCeZ+D4rC2HmS4LdBzXb+BBuHAZTysCo9QClk1t9VuVwwKyorU7ZA== + +"@nx/nx-linux-arm64-musl@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.10.tgz#6f3cbefbe733a09cbec4d72e6f924e56c66ff1ed" + integrity sha512-8GNi8v6Ej/TyAaVBiFRAwFYrqdje/dyFbPzpq895m3ow8zbPrYmoz/qizrgkRzBFulgkSxu33LXi388reVLi2w== + +"@nx/nx-linux-x64-gnu@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.10.tgz#b2eb03ee33fcc53a05cecda7cd77c745def42f92" + integrity sha512-spcDvoQK/mrSvnHFdophlwgzUoa8lYXAZTk15+w/gLpaowP5hR4mInhUWsU9VuEoe41J3x+FcFwmw01Djj6KNQ== + +"@nx/nx-linux-x64-musl@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.10.tgz#50a2da0722cb641d76bf812a2079c0fa1716660a" + integrity sha512-76PdpGIQ1PVA77sYM4Ons779Yx7y3H+Hlsax2HhifAv8dKhAz9/Lmhst9ZqS2zU5v03lGEnN77W4KZQHJBiltA== + +"@nx/nx-win32-arm64-msvc@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.10.tgz#92802c879a43bda1c986c74136a54653675d8ab8" + integrity sha512-dARAojqVyvE88Mt1ms6+UTb3TIAoo4xzkPaX05Qcmh/c8+SQgA24JuqiRp+4kRz1voYwFexRpWpcrfpKJ0Nr+Q== + +"@nx/nx-win32-x64-msvc@22.7.10": + version "22.7.10" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.10.tgz#378b6f7b2f078ccc91d83573af14e65722dcae06" + integrity sha512-qlH70TBMrRw/5pMg6MtPXkuM3LzDHszpqWcJ4NPdipLsyD6T+B+NteanlPtxZrdfqi+8Hd/7fv2OkllA8iY1fg== "@octokit/auth-token@^2.4.4": version "2.5.0" @@ -10874,7 +10874,7 @@ adjust-sourcemap-loader@^4.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -agent-base@6, agent-base@^6.0.2: +agent-base@6, agent-base@6.0.2, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -11546,13 +11546,14 @@ aws-ssl-profiles@^1.1.2: resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== -axios@1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.0.tgz#f8e5dd931cef2a5f8c32216d5784eda2f8750eb7" - integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== +axios@1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.18.1.tgz#d63f9863bcd8938815c86f9e2abd380189d96dfe" + integrity sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g== dependencies: follow-redirects "^1.16.0" form-data "^4.0.5" + https-proxy-agent "^5.0.1" proxy-from-env "^2.1.0" axios@1.20.0: @@ -12010,10 +12011,10 @@ boxen@8.0.1, boxen@^8.0.1: widest-line "^5.0.0" wrap-ansi "^9.0.0" -brace-expansion@5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" - integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== +brace-expansion@5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.8.tgz#135ad0d8d808eb18eb5e0ec9a21f3a0b92ef18cf" + integrity sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg== dependencies: balanced-match "^4.0.2" @@ -21043,10 +21044,10 @@ nwsapi@^2.2.12, nwsapi@^2.2.16, nwsapi@^2.2.4: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.24.tgz#f8927043d4c9b516abdebe804a32c8d1f9484d1f" integrity sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A== -nx@22.7.7: - version "22.7.7" - resolved "https://registry.yarnpkg.com/nx/-/nx-22.7.7.tgz#b93dd707a3f0d966012aa62ad0323e43de2c47e6" - integrity sha512-T4hq5PMAPXeqAmm0Y6Gr5ApeI2I+T8MflWEKN1cZ12R1Rnq2uEpfOgiwAZ74bHONmNgv6LRG9P9+x8SX2tDIlw== +nx@22.7.10: + version "22.7.10" + resolved "https://registry.yarnpkg.com/nx/-/nx-22.7.10.tgz#246bfc3c23741314636ae3f6dea6c092f8dcbfbe" + integrity sha512-T22WdfbkRi9cLH8a+FkKsNrt540DpNkzkFv1StSsphfu1povyqoVQw9Rpl/poNvnAICNKPwZcpGsy/QXMYkECw== dependencies: "@emnapi/core" "1.4.5" "@emnapi/runtime" "1.4.5" @@ -21056,16 +21057,17 @@ nx@22.7.7: "@tybys/wasm-util" "0.9.0" "@yarnpkg/lockfile" "1.1.0" "@zkochan/js-yaml" "0.0.7" + agent-base "6.0.2" ansi-colors "4.1.3" ansi-regex "5.0.1" ansi-styles "4.3.0" argparse "2.0.1" asynckit "0.4.0" - axios "1.16.0" + axios "1.18.1" balanced-match "4.0.3" base64-js "1.5.1" bl "4.1.0" - brace-expansion "5.0.6" + brace-expansion "5.0.8" buffer "5.7.1" call-bind-apply-helpers "1.0.2" chalk "4.1.2" @@ -21076,6 +21078,7 @@ nx@22.7.7: color-convert "2.0.1" color-name "1.1.4" combined-stream "1.0.8" + debug "4.4.3" defaults "1.0.4" define-lazy-prop "2.0.0" delayed-stream "1.0.0" @@ -21106,6 +21109,7 @@ nx@22.7.7: has-symbols "1.1.0" has-tostringtag "1.0.2" hasown "2.0.4" + https-proxy-agent "5.0.1" ieee754 "1.2.1" ignore "7.0.5" inherits "2.0.4" @@ -21124,6 +21128,7 @@ nx@22.7.7: mimic-fn "2.1.0" minimatch "10.2.5" minimist "1.2.8" + ms "2.1.3" npm-run-path "4.0.1" once "1.4.0" onetime "5.1.2" @@ -21159,16 +21164,16 @@ nx@22.7.7: yargs "17.7.2" yargs-parser "21.1.1" optionalDependencies: - "@nx/nx-darwin-arm64" "22.7.7" - "@nx/nx-darwin-x64" "22.7.7" - "@nx/nx-freebsd-x64" "22.7.7" - "@nx/nx-linux-arm-gnueabihf" "22.7.7" - "@nx/nx-linux-arm64-gnu" "22.7.7" - "@nx/nx-linux-arm64-musl" "22.7.7" - "@nx/nx-linux-x64-gnu" "22.7.7" - "@nx/nx-linux-x64-musl" "22.7.7" - "@nx/nx-win32-arm64-msvc" "22.7.7" - "@nx/nx-win32-x64-msvc" "22.7.7" + "@nx/nx-darwin-arm64" "22.7.10" + "@nx/nx-darwin-x64" "22.7.10" + "@nx/nx-freebsd-x64" "22.7.10" + "@nx/nx-linux-arm-gnueabihf" "22.7.10" + "@nx/nx-linux-arm64-gnu" "22.7.10" + "@nx/nx-linux-arm64-musl" "22.7.10" + "@nx/nx-linux-x64-gnu" "22.7.10" + "@nx/nx-linux-x64-musl" "22.7.10" + "@nx/nx-win32-arm64-msvc" "22.7.10" + "@nx/nx-win32-x64-msvc" "22.7.10" nypm@^0.6.0, nypm@^0.6.8, nypm@^0.6.9: version "0.6.9" From a819a8b1e6e27c45fd2f31a6f6478812de13c53b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Oct 2026 17:10:13 +0200 Subject: [PATCH 58/73] fix(deps): Bump seroval to 1.6.8 to fix two Dependabot alerts (#25104) Bumps the hoisted `seroval` and `seroval-plugins` lockfile entries from 1.6.2 to 1.6.8. This resolves two Dependabot alerts: - GHSA-p6vx-979v-rg4c (critical, CVE-2026-104846): `fromJSON()` promise thenable assimilation invokes plugin-produced callables. https://github.com/getsentry/sentry-javascript/security/dependabot/2704 - GHSA-jp82-f5mq-hwhp (high, CVE-2026-104845): memory exhaustion via unchecked TypedArray length in JSON deserialization. https://github.com/getsentry/sentry-javascript/security/dependabot/2703 Every range that requests the hoisted copy is a caret range (`^1.4.0`, `^1.5.0`, `^1.5.4`, `^1.6.2`), so only the lockfile entry needed re-resolving. There is no `package.json` change and `yarn dedupe-deps:check` still passes. `seroval` reaches the tree only through devDependencies of the framework packages, so it does not ship in any published `@sentry/*` package: packages/solidstart (dev) -> @solidjs/start -> seroval packages/vue (dev) -> @tanstack/vue-router -> @tanstack/router-core -> seroval packages/solid (dev) -> @tanstack/solid-router -> @tanstack/router-core -> seroval packages/nuxt (dev) -> nuxt -> @nuxt/vite-builder -> seroval One thing for reviewers to note: a second, nested copy at `solid-js -> seroval@1.5.4` stays unpatched. `solid-js` pins `seroval` to `~1.5.4` even on its latest release (1.9.15), and the highest 1.5.x is 1.5.6, which is still inside both vulnerable ranges. There is no upstream fix for that path yet, so the two alerts may remain open until Solid loosens its pin. That copy is also dev only. Co-authored-by: Claude Opus 5 --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 10ded9708446..0db76c2a23fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24767,9 +24767,9 @@ serialize-javascript@^7.0.3: integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw== seroval-plugins@^1.4.0, seroval-plugins@^1.5.0, seroval-plugins@^1.5.4: - version "1.6.2" - resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.6.2.tgz#8e49b47fd585c2025f74f84f78272ea1753a8daa" - integrity sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ== + version "1.6.8" + resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.6.8.tgz#3187b90331153472bdba83da5be63912e14cd2ac" + integrity sha512-N7mWAMydj89EnYTHtRpS3LBrAz3J9O6oVrqpuXuvlAHfAUG8WEKqtbAd7SBiJNY5NfCAFhSdyAh7wI4Shjqy4g== seroval-plugins@~1.5.0: version "1.5.4" @@ -24777,9 +24777,9 @@ seroval-plugins@~1.5.0: integrity sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw== seroval@^1.4.0, seroval@^1.5.0, seroval@^1.5.4, seroval@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.6.2.tgz#93ecff62ca1312a565e37146b2a332b9d625194d" - integrity sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ== + version "1.6.8" + resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.6.8.tgz#916c4a293cfadcd9e0efdb73a8bfbe061f81866b" + integrity sha512-HlSgSAkTk4EqHcje1ptJjfZi1YDv5KbhVJ/d3P7T/nAXua2VmDu+AKDX5VTdFfZf48nDkWB2TKYt0DrCSa+3wg== seroval@~1.5.0: version "1.5.4" From d3986589d593e36285021163a9904efb12e0635f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 6 Oct 2026 17:11:31 +0200 Subject: [PATCH 59/73] fix(tanstackstart-react): Fix server route parametrization gaps (#25071) Server route parametrization only read `src/routeTree.gen.ts` and only understood `$param` segments, so apps with a custom `srcDirectory`, index routes, brace params and splats kept raw URLs as span names. The matcher now follows TanStack's segment syntax and picks the most specific match, checked against TanStack Router's own matching. Index routes are named with their trailing slash (e.g. `GET /artists/`), matching TanStack's route ids and the client-side names. Fixes #25067 --------- Co-authored-by: Claude Opus 5.5 --- .../{src => app}/client.tsx | 0 .../{src => app}/early-side-effect.ts | 0 .../{src => app}/globals.d.ts | 0 .../{src => app}/instrument.client.ts | 0 .../{src => app}/middleware.ts | 0 .../{src => app}/router.tsx | 0 .../{src => app}/routes/__root.tsx | 0 .../{src => app}/routes/api.db-ioredis.ts | 0 .../{src => app}/routes/api.db-mysql.ts | 0 .../{src => app}/routes/api.error.ts | 0 .../{src => app}/routes/api.flush.ts | 0 .../routes/api.test-middleware.ts | 0 .../{src => app}/routes/api.user.$id.ts | 0 .../routes/crash-before-hydration.tsx | 0 .../{src => app}/routes/custom-monitor.ts | 0 .../{src => app}/routes/index.tsx | 0 .../{src => app}/routes/param.$id.tsx | 0 .../{src => app}/routes/ssr-error.tsx | 0 .../{src => app}/routes/test-middleware.tsx | 0 .../{src => app}/routes/test-serverFn.tsx | 0 .../{src => app}/routes/users.$userId.tsx | 0 .../{src => app}/routes/users.tsx | 0 .../{src => app}/server.ts | 0 .../{src => app}/start.ts | 0 .../tests/transaction.test.ts | 4 +- .../tanstackstart-react-static/tsconfig.json | 2 +- .../tanstackstart-react-static/vite.config.ts | 3 +- .../src/routes/api.files.$.ts | 13 ++ .../src/routes/api.sitemap-{$page}[.]xml.ts | 13 ++ .../src/routes/artists.index.tsx | 9 + .../tests/route-parametrization.test.ts | 47 +++++ .../src/server/routeParametrization.ts | 189 +++++++++++++++--- .../src/vite/routePatterns.ts | 43 ++-- .../test/server/routeParametrization.test.ts | 101 +++++++++- .../test/vite/routePatterns.test.ts | 60 ++++-- 35 files changed, 422 insertions(+), 62 deletions(-) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/client.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/early-side-effect.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/globals.d.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/instrument.client.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/middleware.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/router.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/__root.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/api.db-ioredis.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/api.db-mysql.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/api.error.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/api.flush.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/api.test-middleware.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/api.user.$id.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/crash-before-hydration.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/custom-monitor.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/index.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/param.$id.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/ssr-error.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/test-middleware.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/test-serverFn.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/users.$userId.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/routes/users.tsx (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/server.ts (100%) rename dev-packages/e2e-tests/test-applications/tanstackstart-react-static/{src => app}/start.ts (100%) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/client.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/client.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/early-side-effect.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/early-side-effect.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/globals.d.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/globals.d.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/instrument.client.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/instrument.client.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/middleware.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/middleware.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/router.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/router.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/__root.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/__root.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-ioredis.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-ioredis.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-mysql.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.db-mysql.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.error.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.error.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.flush.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.flush.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.test-middleware.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.test-middleware.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.user.$id.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/api.user.$id.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/crash-before-hydration.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/crash-before-hydration.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/custom-monitor.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/custom-monitor.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/index.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/index.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/param.$id.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/param.$id.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/ssr-error.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/ssr-error.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-middleware.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-middleware.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-serverFn.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/test-serverFn.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.$userId.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.$userId.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/routes/users.tsx diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/server.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/start.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts rename to dev-packages/e2e-tests/test-applications/tanstackstart-react-static/app/start.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts index f13413b3077d..c92d813212b7 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts @@ -36,7 +36,7 @@ test('Sends a server function transaction with auto-instrumentation', async ({ p 'code.function.name': 'testLog', 'http.request.method': 'GET', 'tanstackstart.function.id': expect.any(String), - 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', + 'tanstackstart.function.filename': 'app/routes/test-serverFn.tsx', }, status: 'ok', }), @@ -77,7 +77,7 @@ test('Sends a server function transaction for a nested server function only if i 'code.function.name': 'testNestedLog', 'http.request.method': 'GET', 'tanstackstart.function.id': expect.any(String), - 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx', + 'tanstackstart.function.filename': 'app/routes/test-serverFn.tsx', }, status: 'ok', }), diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json index 5dcdb1fa6f4a..eb48d2313365 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json @@ -21,6 +21,6 @@ "noFallthroughCasesInSwitch": true, "strictNullChecks": true }, - "include": ["src"], + "include": ["app"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts index 2385d8aa5e93..0eef43b166ae 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts @@ -45,7 +45,8 @@ export default defineConfig({ }, plugins: [ tsConfigPaths(), - tanstackStart(), + // Non-default `srcDirectory` moves the generated route tree, which route parametrization has to find + tanstackStart({ srcDirectory: 'app' }), nitro(), // react's vite plugin must come after start's vite plugin viteReact(), diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts new file mode 100644 index 000000000000..08c510a2c3a6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.files.$.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/files/$')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(JSON.stringify({ path: params._splat }), { + headers: { 'Content-Type': 'application/json' }, + }); + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts new file mode 100644 index 000000000000..1885acdad7d5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.sitemap-{$page}[.]xml.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/sitemap-{$page}.xml')({ + server: { + handlers: { + GET: async ({ params }) => { + return new Response(``, { + headers: { 'Content-Type': 'application/xml' }, + }); + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx new file mode 100644 index 000000000000..749ca7f6d79c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/artists.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/artists/')({ + component: ArtistsPage, +}); + +function ArtistsPage() { + return

Artists

; +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts index a1e63e1d9ec2..775624bffc0b 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/route-parametrization.test.ts @@ -72,3 +72,50 @@ test('should parametrize API route span names', async ({ baseURL }) => { expect(serverSpan.name).toBe('GET /api/user/$id'); expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); }); + +test('should parametrize server span names for index routes', async ({ page }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/artists'; + }); + + await page.goto('/artists'); + + const serverSpan = await serverSpanPromise; + + expect(serverSpan.name).toBe('GET /artists/'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); +}); + +test('should parametrize server span names for params with a prefix and suffix', async ({ baseURL }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return ( + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/api/sitemap-') + ); + }); + + await fetch(`${baseURL}/api/sitemap-3.xml`); + + const serverSpan = await serverSpanPromise; + + expect(serverSpan.name).toBe('GET /api/sitemap-{$page}.xml'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); +}); + +test('should parametrize server span names for splat routes', async ({ baseURL }) => { + const serverSpanPromise = waitForStreamedSpan('tanstackstart-react', span => { + return ( + span.is_segment && + getSpanOp(span) === 'http.server' && + String(span.attributes['url.path']?.value ?? '').includes('/api/files/') + ); + }); + + await fetch(`${baseURL}/api/files/a/b/c.txt`); + + const serverSpan = await serverSpanPromise; + + expect(serverSpan.name).toBe('GET /api/files/$'); + expect(serverSpan.attributes['sentry.segment.name.source']).toEqual({ type: 'string', value: 'route' }); +}); diff --git a/packages/tanstackstart-react/src/server/routeParametrization.ts b/packages/tanstackstart-react/src/server/routeParametrization.ts index 8e39a05fcc57..c47b5ddd3e32 100644 --- a/packages/tanstackstart-react/src/server/routeParametrization.ts +++ b/packages/tanstackstart-react/src/server/routeParametrization.ts @@ -1,40 +1,175 @@ import { SENTRY_SEGMENT_NAME_SOURCE, HTTP_ROUTE } from '@sentry/conventions/attributes'; -import { - escapeStringForRegex, - getActiveSpan, - getCurrentScope, - getRootSpan, - spanToJSON, - updateSpanName, -} from '@sentry/core'; - -function patternToRegex(pattern: string): RegExp { - const segments = pattern - .split('/') - .map(segment => { - if (segment.startsWith('$')) { - return '[^/]+'; +import { getActiveSpan, getCurrentScope, getRootSpan, spanToJSON, updateSpanName } from '@sentry/core'; + +type RouteSegment = + | { kind: 'static'; value: string } + | { kind: 'param' | 'optional' | 'splat'; prefix: string; suffix: string }; + +interface ParsedRoutePattern { + segments: RouteSegment[]; + isIndex: boolean; + affixLength: number; +} + +interface MatchScore { + statics: number; + dynamics: number; + optionals: number; +} + +// e.g. `@{$groupSlug}`, `sitemap-{$page}.xml`, `{-$locale}`, `{$}` +const BRACED_SEGMENT_REGEX = /^(.*?)\{(-?)\$([^}]*)\}(.*)$/; + +const parsedRoutePatterns = new Map(); + +function parseRoutePattern(pattern: string): ParsedRoutePattern { + const cached = parsedRoutePatterns.get(pattern); + if (cached) { + return cached; + } + + const parts = pattern.split('/').filter(Boolean); + const segments: RouteSegment[] = []; + let affixLength = 0; + for (let i = 0; i < parts.length; i++) { + const part = parts[i] as string; + const braced = part.match(BRACED_SEGMENT_REGEX); + if (part === '$' || (braced && !braced[3] && !braced[2])) { + // A splat consumes the rest of the path, so anything after it in the pattern is part of its suffix. + const rest = parts.slice(i + 1).join('/'); + const prefix = braced?.[1] ?? ''; + const suffix = (braced?.[4] ?? '') + (rest ? `/${rest}` : ''); + segments.push({ kind: 'splat', prefix: prefix.toLowerCase(), suffix: suffix.toLowerCase() }); + affixLength += prefix.length + suffix.length; + break; + } + if (part.startsWith('$')) { + segments.push({ kind: 'param', prefix: '', suffix: '' }); + } else if (braced?.[3]) { + const [, prefix = '', optional, , suffix = ''] = braced; + segments.push({ + kind: optional ? 'optional' : 'param', + prefix: prefix.toLowerCase(), + suffix: suffix.toLowerCase(), + }); + affixLength += prefix.length + suffix.length; + } else { + segments.push({ kind: 'static', value: part.toLowerCase() }); + } + } + + const parsed = { segments, isIndex: pattern.endsWith('/'), affixLength }; + parsedRoutePatterns.set(pattern, parsed); + return parsed; +} + +function hasAffixes(value: string, prefix: string, suffix: string): boolean { + return value.startsWith(prefix) && value.endsWith(suffix) && value.length >= prefix.length + suffix.length; +} + +function isBetterScore(a: MatchScore, b: MatchScore): boolean { + if (a.statics !== b.statics) { + return a.statics > b.statics; + } + if (a.dynamics !== b.dynamics) { + return a.dynamics > b.dynamics; + } + return a.optionals > b.optionals; +} + +/** + * Returns the best score with which `segments` match the remaining path `parts`, or `undefined` if they don't match. + * Like TanStack Router, a path part matched by a static, param or optional segment adds to the respective score, + * weighted so that earlier parts outweigh all later ones. + */ +function scoreMatch( + segments: RouteSegment[], + parts: string[], + si: number, + pi: number, + score: MatchScore, +): MatchScore | undefined { + const segment = segments[si]; + if (!segment) { + return pi === parts.length ? score : undefined; + } + + const part = parts[pi]; + const weight = 2 ** (parts.length - pi - 1); + + switch (segment.kind) { + case 'static': + return part === segment.value + ? scoreMatch(segments, parts, si + 1, pi + 1, { ...score, statics: score.statics + weight }) + : undefined; + case 'param': + return part !== undefined && hasAffixes(part, segment.prefix, segment.suffix) + ? scoreMatch(segments, parts, si + 1, pi + 1, { ...score, dynamics: score.dynamics + weight }) + : undefined; + case 'optional': { + const skipped = scoreMatch(segments, parts, si + 1, pi, score); + const consumed = + part !== undefined && hasAffixes(part, segment.prefix, segment.suffix) + ? scoreMatch(segments, parts, si + 1, pi + 1, { ...score, optionals: score.optionals + weight }) + : undefined; + if (!skipped || !consumed) { + return skipped || consumed; + } + return isBetterScore(consumed, skipped) ? consumed : skipped; + } + case 'splat': { + if (!segment.prefix && !segment.suffix) { + return score; } - return escapeStringForRegex(segment); - }) - .join('/'); - return new RegExp(`^${segments}$`); + return part !== undefined && hasAffixes(parts.slice(pi).join('/'), segment.prefix, segment.suffix) + ? score + : undefined; + } + } +} + +function isMoreSpecific( + a: { score: MatchScore; route: ParsedRoutePattern }, + b: { score: MatchScore; route: ParsedRoutePattern }, +): boolean { + if ( + a.score.statics !== b.score.statics || + a.score.dynamics !== b.score.dynamics || + a.score.optionals !== b.score.optionals + ) { + return isBetterScore(a.score, b.score); + } + if (a.route.isIndex !== b.route.isIndex) { + return a.route.isIndex; + } + if (a.route.segments.length !== b.route.segments.length) { + return a.route.segments.length > b.route.segments.length; + } + return a.route.affixLength > b.route.affixLength; } /** - * Matches a URL pathname against a list of TanStack Start route patterns. - * Patterns use `$param` syntax for dynamic segments (e.g., `/users/$id`). - * - * Patterns are expected to be pre-sorted by specificity (more segments first, static before dynamic). + * Matches a URL pathname against a list of TanStack Start route patterns (the `fullPaths` of the generated route tree). + * Supports `$param`, `prefix{$param}suffix`, optional `{-$param}` and splat (`$`, `{$}`) segments, as well as + * index routes (trailing slash). If several patterns match, the most specific one is picked the way TanStack Router + * picks it. */ export function matchUrlToRoutePattern(pathname: string, patterns: string[]): string | undefined { - const normalizedPathname = pathname.length > 1 ? pathname.replace(/\/$/, '') : pathname; + if (pathname === '/' && patterns.includes('/')) { + return '/'; + } + + // TanStack Router matches case-insensitively by default. + const parts = pathname.toLowerCase().split('/').filter(Boolean); + let best: { pattern: string; score: MatchScore; route: ParsedRoutePattern } | undefined; for (const pattern of patterns) { - if (patternToRegex(pattern).test(normalizedPathname)) { - return pattern; + const route = parseRoutePattern(pattern); + const score = scoreMatch(route.segments, parts, 0, 0, { statics: 0, dynamics: 0, optionals: 0 }); + if (score && (!best || isMoreSpecific({ score, route }, best))) { + best = { pattern, score, route }; } } - return undefined; + return best?.pattern; } /** diff --git a/packages/tanstackstart-react/src/vite/routePatterns.ts b/packages/tanstackstart-react/src/vite/routePatterns.ts index 9c90b863931c..5ffbdaa83c52 100644 --- a/packages/tanstackstart-react/src/vite/routePatterns.ts +++ b/packages/tanstackstart-react/src/vite/routePatterns.ts @@ -3,6 +3,8 @@ import * as path from 'node:path'; import { uniq } from '@sentry/core'; import type { Plugin } from 'vite'; +const ROUTE_TREE_FILE_NAME = 'routeTree.gen.ts'; + /** * Extracts route patterns from TanStack Start's generated routeTree.gen.ts * and replaces `__SENTRY_ROUTE_PATTERNS__` references with the extracted patterns. @@ -28,11 +30,10 @@ export function makeRoutePatternPlugin(): Plugin { return null; } - // extract the patterns from the route tree file - const routeTreePath = path.resolve(resolvedRoot, 'src/routeTree.gen.ts'); let patterns: string[] = []; try { - if (fs.existsSync(routeTreePath)) { + const routeTreePath = findRouteTreeFile(resolvedRoot); + if (routeTreePath) { patterns = extractRoutePatterns(fs.readFileSync(routeTreePath, 'utf-8')); } } catch { @@ -47,6 +48,31 @@ export function makeRoutePatternPlugin(): Plugin { }; } +/** + * Finds the generated route tree. TanStack Start writes it to `/routeTree.gen.ts`, and the + * `srcDirectory` option (default `src`) is not exposed by its Vite plugin, so we check `src` first and then + * every other top-level directory. + */ +export function findRouteTreeFile(root: string): string | undefined { + const defaultPath = path.join(root, 'src', ROUTE_TREE_FILE_NAME); + if (fs.existsSync(defaultPath)) { + return defaultPath; + } + + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.')) { + continue; + } + const candidate = path.join(root, entry.name, ROUTE_TREE_FILE_NAME); + if (fs.existsSync(candidate)) { + return candidate; + } + } + + const rootPath = path.join(root, ROUTE_TREE_FILE_NAME); + return fs.existsSync(rootPath) ? rootPath : undefined; +} + /** * Extracts full route path patterns from the content of routeTree.gen.ts. * @@ -69,14 +95,5 @@ export function extractRoutePatterns(content: string): string[] { } } - return uniq(patterns).sort((a, b) => { - const aSegments = a.split('/'); - const bSegments = b.split('/'); - if (bSegments.length !== aSegments.length) { - return bSegments.length - aSegments.length; - } - const aDynamic = aSegments.filter(s => s.startsWith('$')).length; - const bDynamic = bSegments.filter(s => s.startsWith('$')).length; - return aDynamic - bDynamic; - }); + return uniq(patterns); } diff --git a/packages/tanstackstart-react/test/server/routeParametrization.test.ts b/packages/tanstackstart-react/test/server/routeParametrization.test.ts index 88fd2643dc9b..d5079cb1bc8c 100644 --- a/packages/tanstackstart-react/test/server/routeParametrization.test.ts +++ b/packages/tanstackstart-react/test/server/routeParametrization.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { matchUrlToRoutePattern } from '../../src/server/routeParametrization'; describe('matchUrlToRoutePattern', () => { - // Pre-sorted by specificity: more segments first, static before dynamic const patterns = ['/users/$userId/posts/$postId', '/api/health', '/page-a', '/page-b/$id', '/']; it('matches the root route', () => { @@ -29,13 +28,13 @@ describe('matchUrlToRoutePattern', () => { expect(matchUrlToRoutePattern('/page-b', patterns)).toBeUndefined(); }); - it('prefers static over dynamic matches when pre-sorted', () => { - const patternsWithOverlap = ['/page-b/special', '/page-b/$id']; + it('prefers static over dynamic matches', () => { + const patternsWithOverlap = ['/page-b/$id', '/page-b/special']; expect(matchUrlToRoutePattern('/page-b/special', patternsWithOverlap)).toBe('/page-b/special'); }); - it('prefers more specific routes when pre-sorted', () => { - const patternsNested = ['/users/$id/profile', '/users/$id']; + it('prefers more specific routes', () => { + const patternsNested = ['/users/$id', '/users/$id/profile']; expect(matchUrlToRoutePattern('/users/123/profile', patternsNested)).toBe('/users/$id/profile'); }); @@ -43,4 +42,96 @@ describe('matchUrlToRoutePattern', () => { expect(matchUrlToRoutePattern('/page-a/', patterns)).toBe('/page-a'); expect(matchUrlToRoutePattern('/page-b/42/', patterns)).toBe('/page-b/$id'); }); + + describe('TanStack Router segment grammar', () => { + // `fullPaths` of a route tree generated by @tanstack/router-generator, in generator order. + // Expected matches were checked against `router.matchRoutes()` of @tanstack/react-router. + const routeTreePatterns = [ + '/', + '/$user', + '/@{$groupSlug}', + '/about', + '/blog/latest', + '/blog/{-$tag}', + '/files', + '/posts', + '/pre-{$id}-post', + '/shop/items', + '/sitemap-{$page}.xml', + '/dashboard', + '/docs/', + '/docs/{$}', + '/files/$', + '/files/special', + '/groups/$groupId', + '/posts/$postId', + '/{-$locale}/about', + '/artists/', + '/@{$groupSlug}/covers/$coverId', + '/a/$x/c', + '/a/b/$y', + '/shop/{-$cat}/items', + '/groups/$groupId/', + ]; + + it.each([ + ['/', '/'], + ['/artists', '/artists/'], + ['/artists/', '/artists/'], + ['/ARTISTS', '/artists/'], + ['/groups/abc', '/groups/$groupId/'], + ['/groups/abc/', '/groups/$groupId/'], + ['/@some-group', '/@{$groupSlug}'], + ['/@some-group/covers/c1', '/@{$groupSlug}/covers/$coverId'], + ['/sitemap-3.xml', '/sitemap-{$page}.xml'], + ['/sitemap-.xml', '/sitemap-{$page}.xml'], + ['/pre-1-post', '/pre-{$id}-post'], + ['/about', '/{-$locale}/about'], + ['/en/about', '/{-$locale}/about'], + ['/shop/items', '/shop/{-$cat}/items'], + ['/shop/toys/items', '/shop/{-$cat}/items'], + ['/blog/latest', '/blog/latest'], + ['/blog/foo', '/blog/{-$tag}'], + ['/files', '/files/$'], + ['/files/a/b/c', '/files/$'], + ['/files/special', '/files/special'], + ['/docs', '/docs/'], + ['/docs/x/y', '/docs/{$}'], + ['/a/b/c', '/a/b/$y'], + ['/a/z/c', '/a/$x/c'], + ['/john', '/$user'], + ['/posts/1', '/posts/$postId'], + ['/dashboard', '/dashboard'], + ])('matches %s to %s', (pathname, expected) => { + expect(matchUrlToRoutePattern(pathname, routeTreePatterns)).toBe(expected); + }); + + it('does not match paths no route covers', () => { + expect(matchUrlToRoutePattern('/nope/nope/nope', routeTreePatterns)).toBeUndefined(); + }); + + it.each([ + ['/', '/'], + ['/about', '/about'], + ['/pricing', '/pricing'], + ['/en', '/{-$locale}/$slug'], + ['/hello', '/{-$locale}/$slug'], + ['/en/hello', '/{-$locale}/$slug'], + ['/posts', '/posts/'], + ['/posts/new', '/posts/new'], + ['/posts/1', '/posts/$postId'], + ])('prefers static over param over optional segments when matching %s', (pathname, expected) => { + const patterns = [ + '/', + '/about', + '/pricing', + '/posts/$postId', + '/posts/new', + '/{-$locale}/$slug', + '/posts/', + '/{-$locale}/', + ]; + expect(matchUrlToRoutePattern(pathname, patterns)).toBe(expected); + }); + }); }); diff --git a/packages/tanstackstart-react/test/vite/routePatterns.test.ts b/packages/tanstackstart-react/test/vite/routePatterns.test.ts index 07b1c2803bff..413b29adafe1 100644 --- a/packages/tanstackstart-react/test/vite/routePatterns.test.ts +++ b/packages/tanstackstart-react/test/vite/routePatterns.test.ts @@ -1,5 +1,8 @@ -import { describe, expect, it } from 'vitest'; -import { extractRoutePatterns } from '../../src/vite/routePatterns'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { extractRoutePatterns, findRouteTreeFile } from '../../src/vite/routePatterns'; describe('extractRoutePatterns', () => { it('extracts full path patterns from single-line fullPaths union', () => { @@ -57,17 +60,6 @@ export interface FileRouteTypes { expect(patterns).toContain('/users/$userId/posts/$postId'); }); - it('sorts patterns by specificity: more segments first, static before dynamic', () => { - const content = ` -export interface FileRouteTypes { - fullPaths: '/' | '/page-b/$id' | '/page-b/special' | '/users/$id/profile' | '/users/$id' - fileRoutesByTo: FileRoutesByTo -} -`; - const patterns = extractRoutePatterns(content); - expect(patterns).toEqual(['/users/$id/profile', '/page-b/special', '/page-b/$id', '/users/$id', '/']); - }); - it('handles double-quoted paths (quoteStyle: "double")', () => { const content = ` export interface FileRouteTypes { @@ -92,3 +84,45 @@ export interface FileRouteTypes { expect(patterns.filter(p => p === '/page-a')).toHaveLength(1); }); }); + +describe('findRouteTreeFile', () => { + let root: string; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-tanstackstart-route-tree-')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + function writeRouteTree(dir: string): string { + const filePath = path.join(root, dir, 'routeTree.gen.ts'); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, ''); + return filePath; + } + + it('finds the route tree in the default `src` directory', () => { + const expected = writeRouteTree('src'); + writeRouteTree('app'); + expect(findRouteTreeFile(root)).toBe(expected); + }); + + it('finds the route tree in a custom `srcDirectory`', () => { + fs.mkdirSync(path.join(root, 'src')); + const expected = writeRouteTree('app'); + expect(findRouteTreeFile(root)).toBe(expected); + }); + + it('finds the route tree in the project root', () => { + const expected = writeRouteTree('.'); + expect(findRouteTreeFile(root)).toBe(expected); + }); + + it('ignores node_modules and dot directories', () => { + writeRouteTree('node_modules'); + writeRouteTree('.output'); + expect(findRouteTreeFile(root)).toBeUndefined(); + }); +}); From 99d3ed65f363420c9e75d5046d977b81fb03c542 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 6 Oct 2026 17:18:44 +0200 Subject: [PATCH 60/73] test(node): Port Apollo GraphQL integration tests to span streaming (#25030) Port the Apollo GraphQL integration suites to span streaming. Streaming keeps the root name unchanged and records operation names as attributes. Keep focused transaction coverage in `naming-static`. Closes #24139 --------- Co-authored-by: GPT-6 --- .../tracing/apollo-graphql/instrument.mjs | 1 - .../instrument.mjs} | 4 +- .../apollo-graphql/naming-static/test.ts | 69 +++++ .../apollo-graphql/resolvers/instrument.mjs | 1 - .../tracing/apollo-graphql/resolvers/test.ts | 131 +++------ .../suites/tracing/apollo-graphql/test.ts | 220 ++++++++------ .../instrument.mjs | 1 - .../useOperationNameForRootSpan/test.ts | 276 ++++++++++-------- 8 files changed, 398 insertions(+), 305 deletions(-) rename dev-packages/node-integration-tests/suites/tracing/apollo-graphql/{resolvers/instrument-dc.mjs => naming-static/instrument.mjs} (69%) create mode 100644 dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs index 170ad6f6a702..46a27dd03b74 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument-dc.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/instrument.mjs similarity index 69% rename from dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument-dc.mjs rename to dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/instrument.mjs index bfdb2468109c..29122b580fae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument-dc.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/instrument.mjs @@ -1,11 +1,9 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; -// `graphqlIntegration()` is the diagnostics-channel implementation by default; configure it with -// `ignoreResolveSpans: false` so resolver spans are captured. Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', + traceLifecycle: 'static', release: '1.0', tracesSampleRate: 1.0, integrations: [Sentry.graphqlIntegration({ ignoreResolveSpans: false })], diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts new file mode 100644 index 000000000000..752bb091892c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/naming-static/test.ts @@ -0,0 +1,69 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('GraphQL/Apollo Tests > static naming', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests( + __dirname, + '../useOperationNameForRootSpan/scenario-query.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('keeps operation and resolver names in transaction spans', async () => { + await createTestRunner() + .expect({ transaction: { transaction: 'Test Server Start' } }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('test span name (query GetHello)'); + const spans = event.spans?.filter(span => span.op === 'graphql'); + expect(spans?.map(span => span.description).sort()).toEqual([ + 'graphql.parse', + 'graphql.resolve hello', + 'graphql.validate', + 'query GetHello', + ]); + }, + }) + .start() + .completed(); + }); + }, + ); + + createEsmAndCjsTests( + __dirname, + '../useOperationNameForRootSpan/scenario-multiple-operations.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('sorts operation names in the transaction name', async () => { + await createTestRunner() + .expect({ transaction: { transaction: 'Test Server Start' } }) + .expect({ transaction: { transaction: 'test span name (query GetHello, query GetWorld)' } }) + .start() + .completed(); + }); + }, + ); + + createEsmAndCjsTests( + __dirname, + '../useOperationNameForRootSpan/scenario-multiple-operations-many.mjs', + 'instrument.mjs', + (createTestRunner, test) => { + test('truncates the transaction name after five operations', async () => { + await createTestRunner() + .expect({ transaction: { transaction: 'Test Server Start' } }) + .expect({ + transaction: { + transaction: + 'test span name (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)', + }, + }) + .start() + .completed(); + }); + }, + ); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs index c69cfdcd4622..aed7c6310653 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts index 6021abe5aabe..b3603e18873e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/resolvers/test.ts @@ -1,105 +1,56 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -// Server start transaction (Apollo Server v5 no longer runs introspection query on start) -const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', -}; - describe('GraphQL/Apollo Tests > resolve spans', () => { afterAll(() => { cleanupChildProcesses(); }); - // With `ignoreResolveSpans: false`, the instrumentation emits a span for the execute step as well as - // for `parse`, `validate` and each (non-trivial) field resolver. - const EXPECTED_TRANSACTION = { - // `useOperationNameForRootSpan` defaults to true, so the root span name gets the operation appended. - transaction: 'Test Transaction (query)', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'query', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining({ - 'graphql.operation.type': 'query', - 'graphql.processing.type': 'execute', - 'graphql.document': '{hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - }), - }), - expect.objectContaining({ - description: 'graphql.parse', - data: expect.objectContaining({ 'graphql.processing.type': 'parse' }), - }), - expect.objectContaining({ - description: 'graphql.validate', - data: expect.objectContaining({ 'graphql.processing.type': 'validate' }), - }), - expect.objectContaining({ - description: 'graphql.resolve hello', - data: expect.objectContaining({ - 'graphql.processing.type': 'resolve', - 'graphql.field.name': 'hello', - 'graphql.field.path': 'hello', - 'graphql.field.type': 'String', - 'graphql.parent.name': 'Query', - }), - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument.mjs', (createTestRunner, test) => { test('emits parse, validate and resolve spans when ignoreResolveSpans is false', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - }); + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); - // Same behavior on the diagnostics-channel path: passing the channel integration explicitly (see - // instrument-dc.mjs) must carry `ignoreResolveSpans: false` through — the explicit instance wins over - // the swapped-in default — and emit resolve spans with the orchestrion origin. - const EXPECTED_ORCHESTRION_TRANSACTION = { - transaction: 'Test Transaction (query)', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'query', - origin: 'auto.graphql.diagnostic_channel', - data: expect.objectContaining({ - 'graphql.operation.type': 'query', - 'graphql.processing.type': 'execute', - 'graphql.document': '{hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - }), - }), - expect.objectContaining({ - description: 'graphql.parse', - data: expect.objectContaining({ 'graphql.processing.type': 'parse' }), - }), - expect.objectContaining({ - description: 'graphql.validate', - data: expect.objectContaining({ 'graphql.processing.type': 'validate' }), - }), - expect.objectContaining({ - description: 'graphql.resolve hello', - data: expect.objectContaining({ - 'graphql.processing.type': 'resolve', - 'graphql.field.name': 'hello', - 'graphql.field.path': 'hello', - 'graphql.field.type': 'String', - 'graphql.parent.name': 'Query', - }), - }), - ]), - }; - - createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument-dc.mjs', (createTestRunner, test) => { - test('emits resolve spans via diagnostics-channel injection when configured explicitly', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_ORCHESTRION_TRANSACTION }) + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'query', type: 'string' }); + const executeSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'execute'); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{hello}', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + const parseSpan = children.find(span => span.attributes['graphql.processing.type']?.value === 'parse'); + expect(parseSpan?.name).toBe('GraphQL parse'); + expect(parseSpan?.attributes['graphql.processing.type']).toEqual({ value: 'parse', type: 'string' }); + const validateSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'validate', + ); + expect(validateSpan?.name).toBe('GraphQL validate'); + expect(validateSpan?.attributes['graphql.processing.type']).toEqual({ value: 'validate', type: 'string' }); + const helloResolverSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'resolve' && + span.attributes['graphql.field.path']?.value === 'hello', + ); + expect(helloResolverSpan?.name).toBe('GraphQL resolve'); + expect(helloResolverSpan?.attributes['graphql.processing.type']).toEqual({ + value: 'resolve', + type: 'string', + }); + expect(helloResolverSpan?.attributes['graphql.field.name']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.field.path']).toEqual({ value: 'hello', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.field.type']).toEqual({ value: 'String', type: 'string' }); + expect(helloResolverSpan?.attributes['graphql.parent.name']).toEqual({ value: 'Query', type: 'string' }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts index 4b60ccf01f3a..40193bff052a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts @@ -1,47 +1,12 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -// Server start transaction (Apollo Server v5 no longer runs introspection query on start) -const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', -}; - -const ORIGIN = 'auto.graphql.diagnostic_channel'; - -function graphqlExecuteSpan(opts: { - description: string; - operationType: string; - operationName?: string; - document: unknown; - status?: string; -}): ReturnType { - const { description, operationType, operationName, document, status = 'ok' } = opts; - return expect.objectContaining({ - description, - status, - origin: ORIGIN, - data: expect.objectContaining({ - 'graphql.operation.type': operationType, - ...(operationName ? { 'graphql.operation.name': operationName } : {}), - 'graphql.document': document, - 'sentry.origin': ORIGIN, - }), - }); -} - describe('GraphQL/Apollo Tests', () => { afterAll(() => { cleanupChildProcesses(); }); describe('query', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (query)', - spans: expect.arrayContaining([ - graphqlExecuteSpan({ description: 'query', operationType: 'query', document: '{hello}' }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-query.mjs', @@ -49,9 +14,30 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('should instrument GraphQL queries used from Apollo Server.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'query', type: 'string' }); + const executeSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'execute', + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: '{hello}', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -61,18 +47,6 @@ describe('GraphQL/Apollo Tests', () => { }); describe('mutation', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (mutation Mutation)', - spans: expect.arrayContaining([ - graphqlExecuteSpan({ - description: 'mutation Mutation', - operationType: 'mutation', - operationName: 'Mutation', - document: 'mutation Mutation($email: String) {\n login(email: $email)\n}', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-mutation.mjs', @@ -80,9 +54,45 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('should instrument GraphQL mutations used from Apollo Server.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'mutation Mutation', + type: 'string', + }); + const mutationSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Mutation', + ); + expect(mutationSpan?.name).toBe('GraphQL mutation'); + expect(mutationSpan?.status).toBe('ok'); + expect(mutationSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'Mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Mutation($email: String) {\n login(email: $email)\n}', + type: 'string', + }); + expect(mutationSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -92,18 +102,6 @@ describe('GraphQL/Apollo Tests', () => { }); describe('redaction', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (mutation)', - spans: expect.arrayContaining([ - // The inline email literal must be redacted to `"*"`, so the raw value never reaches the span. - graphqlExecuteSpan({ - description: 'mutation', - operationType: 'mutation', - document: expect.stringContaining('login(email: "*")'), - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-redaction.mjs', @@ -111,9 +109,36 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('redacts inline literal values from the graphql document.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'mutation', type: 'string' }); + const executeSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'execute', + ); + expect(executeSpan?.name).toBe('GraphQL mutation'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ + value: expect.stringContaining('login(email: "*")'), + type: 'string', + }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -123,19 +148,6 @@ describe('GraphQL/Apollo Tests', () => { }); describe('error', () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction (mutation Mutation)', - spans: expect.arrayContaining([ - graphqlExecuteSpan({ - description: 'mutation Mutation', - operationType: 'mutation', - operationName: 'Mutation', - document: 'mutation Mutation($email: String) {\n login(email: $email)\n}', - status: 'internal_error', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-error.mjs', @@ -143,9 +155,49 @@ describe('GraphQL/Apollo Tests', () => { (createTestRunner, test) => { test('should handle GraphQL errors.', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .unordered() + .expect({ + span: container => { + expect( + container.items.find(span => span.is_segment && span.name === 'Test Server Start'), + ).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'Test Transaction'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'mutation Mutation', + type: 'string', + }); + const mutationSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'Mutation', + ); + expect(mutationSpan?.name).toBe('GraphQL mutation'); + expect(mutationSpan?.status).toBe('error'); + expect(mutationSpan?.attributes['sentry.status.message']).toEqual({ + value: 'internal_error', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'Mutation', + type: 'string', + }); + expect(mutationSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation Mutation($email: String) {\n login(email: $email)\n}', + type: 'string', + }); + expect(mutationSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs index 35d159556ae1..4f8eddd456c8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/instrument.mjs @@ -2,7 +2,6 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts index d414cabfa3d4..9ef795290bae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/useOperationNameForRootSpan/test.ts @@ -1,41 +1,48 @@ import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; -// Server start transaction (Apollo Server v5 no longer runs introspection query on start) -const EXPECTED_START_SERVER_TRANSACTION = { - transaction: 'Test Server Start', -}; - describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { afterAll(() => { cleanupChildProcesses(); }); describe('single query operation', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query GetHello)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetHello', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetHello {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query GetHello', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with single query operation', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'query GetHello', + type: 'string', + }); + const getHelloSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetHello', + ); + expect(getHelloSpan?.name).toBe('GraphQL query'); + expect(getHelloSpan?.status).toBe('ok'); + expect(getHelloSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetHello', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetHello {hello}', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + }, + }) .start() .completed(); }); @@ -43,32 +50,51 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); describe('single mutation operation', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (mutation TestMutation)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'TestMutation', - 'graphql.operation.type': 'mutation', - 'graphql.document': `mutation TestMutation($email: String) { - login(email: $email) -}`, - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'mutation TestMutation', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-mutation.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with single mutation operation', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: 'mutation TestMutation', + type: 'string', + }); + const testMutationSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'TestMutation', + ); + expect(testMutationSpan?.name).toBe('GraphQL mutation'); + expect(testMutationSpan?.status).toBe('ok'); + expect(testMutationSpan?.attributes['graphql.operation.name']).toEqual({ + value: 'TestMutation', + type: 'string', + }); + expect(testMutationSpan?.attributes['graphql.operation.type']).toEqual({ + value: 'mutation', + type: 'string', + }); + expect(testMutationSpan?.attributes['graphql.document']).toEqual({ + value: 'mutation TestMutation($email: String) {\n login(email: $email)\n}', + type: 'string', + }); + expect(testMutationSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(testMutationSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(testMutationSpan?.attributes['graphql.processing.type']).toEqual({ + value: 'execute', + type: 'string', + }); + }, + }) .start() .completed(); }); @@ -76,29 +102,33 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); describe('query without name', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.type': 'query', - 'graphql.document': 'query {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-no-operation-name.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with single query operation without name', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ value: 'query', type: 'string' }); + const executeSpan = children.find( + span => span.attributes['graphql.processing.type']?.value === 'execute', + ); + expect(executeSpan?.name).toBe('GraphQL query'); + expect(executeSpan?.status).toBe('ok'); + expect(executeSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(executeSpan?.attributes['graphql.document']).toEqual({ value: 'query {hello}', type: 'string' }); + expect(executeSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(executeSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(executeSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + }, + }) .start() .completed(); }); @@ -106,68 +136,64 @@ describe('GraphQL/Apollo Tests > useOperationNameForRootSpan', () => { }); describe('multiple operations', () => { - const EXPECTED_TRANSACTION = { - transaction: 'test span name (query GetHello, query GetWorld)', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetHello', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetHello {hello}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query GetHello', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - expect.objectContaining({ - data: { - 'graphql.operation.name': 'GetWorld', - 'graphql.operation.type': 'query', - 'graphql.document': 'query GetWorld {world}', - 'sentry.origin': 'auto.graphql.diagnostic_channel', - 'sentry.op': 'graphql', - 'graphql.processing.type': 'execute', - }, - description: 'query GetWorld', - status: 'ok', - origin: 'auto.graphql.diagnostic_channel', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-multiple-operations.mjs', 'instrument.mjs', (createTestRunner, test) => { test('useOperationNameForRootSpan works with multiple query operations', async () => { await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) + .expect({ + span: container => { + expect(container.items.find(span => span.is_segment && span.name === 'Test Server Start')).toBeDefined(); + const segment = container.items.find(span => span.is_segment && span.name === 'test span name'); + const children = container.items.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segment?.span_id, + ); + + expect(segment?.attributes['sentry.graphql.operation']).toEqual({ + value: ['query GetWorld', 'query GetHello'], + type: 'array', + }); + const getHelloSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetHello', + ); + expect(getHelloSpan?.name).toBe('GraphQL query'); + expect(getHelloSpan?.status).toBe('ok'); + expect(getHelloSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetHello', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetHello {hello}', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getHelloSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getHelloSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + const getWorldSpan = children.find( + span => + span.attributes['graphql.processing.type']?.value === 'execute' && + span.attributes['graphql.operation.name']?.value === 'GetWorld', + ); + expect(getWorldSpan?.name).toBe('GraphQL query'); + expect(getWorldSpan?.status).toBe('ok'); + expect(getWorldSpan?.attributes['graphql.operation.name']).toEqual({ value: 'GetWorld', type: 'string' }); + expect(getWorldSpan?.attributes['graphql.operation.type']).toEqual({ value: 'query', type: 'string' }); + expect(getWorldSpan?.attributes['graphql.document']).toEqual({ + value: 'query GetWorld {world}', + type: 'string', + }); + expect(getWorldSpan?.attributes['sentry.origin']).toEqual({ + value: 'auto.graphql.diagnostic_channel', + type: 'string', + }); + expect(getWorldSpan?.attributes['sentry.op']).toEqual({ value: 'graphql', type: 'string' }); + expect(getWorldSpan?.attributes['graphql.processing.type']).toEqual({ value: 'execute', type: 'string' }); + }, + }) .start() .completed(); }); }); }); - - describe('many operations', () => { - const EXPECTED_TRANSACTION = { - transaction: - 'test span name (query GetHello1, query GetHello2, query GetHello3, query GetHello4, query GetHello5, +4)', - }; - - createEsmAndCjsTests( - __dirname, - 'scenario-multiple-operations-many.mjs', - 'instrument.mjs', - (createTestRunner, test) => { - test('useOperationNameForRootSpan works with more than 5 query operations', async () => { - await createTestRunner() - .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); - }); - }, - ); - }); }); From 019c889e8aeae065a2215d64dc2076f3b27eed09 Mon Sep 17 00:00:00 2001 From: Minwook Shin <163576506+minwookshin@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:19:05 -0400 Subject: [PATCH 61/73] feat(bun): keep server spans open until streaming responses finish (#25025) Bun server spans now remain open until a streaming response completes, errors, or is cancelled. Source failures are captured with their request context and mark the span as failed; client cancellation remains separate. The shared content-type classifier lives in `server-utils`, with Bun and Deno using the same implementation. Targeted classifier, Bun unit/integration and Deno streaming tests pass, along with the four affected package builds/type checks and scoped lint/formatting. Earlier checks found base-reproduced Bun test-typecheck diagnostics and a Deno concurrent-request failure; those broader checks were not rerun for this move. Fixes #23883. --------- Co-authored-by: GPT-6 Co-authored-by: JPeer264 --- .../suites/streaming/index.ts | 33 ++++ .../suites/streaming/test.ts | 83 ++++++++++ packages/bun/src/integrations/bunserver.ts | 24 ++- packages/bun/src/utils/streaming.ts | 40 +++++ .../bun/test/integrations/bunserver.test.ts | 154 +++++++++++++++++- .../src/wrapRequestHandlerWithInit.ts | 2 +- packages/core/src/server.ts | 2 + .../src/utils/responseStreaming.ts} | 0 .../test/lib/utils/responseStreaming.test.ts | 34 ++++ packages/deno/src/utils/streaming.ts | 43 +---- 10 files changed, 366 insertions(+), 49 deletions(-) create mode 100644 dev-packages/bun-integration-tests/suites/streaming/index.ts create mode 100644 dev-packages/bun-integration-tests/suites/streaming/test.ts create mode 100644 packages/bun/src/utils/streaming.ts rename packages/{cloudflare/src/utils/streaming.ts => core/src/utils/responseStreaming.ts} (100%) create mode 100644 packages/core/test/lib/utils/responseStreaming.test.ts diff --git a/dev-packages/bun-integration-tests/suites/streaming/index.ts b/dev-packages/bun-integration-tests/suites/streaming/index.ts new file mode 100644 index 000000000000..aeb5dc2e6460 --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/streaming/index.ts @@ -0,0 +1,33 @@ +import { sendPortToRunner } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/bun'; + +Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1 }); + +const server = Bun.serve({ + port: 0, + fetch(request) { + const path = new URL(request.url).pathname; + if (path === '/error') { + throw new Error('handler failed'); + } + const span = Sentry.getActiveSpan(); + const body = new ReadableStream({ + async start(controller) { + controller.enqueue(new TextEncoder().encode('data: first\n\n')); + await Bun.sleep(50); + if (path === '/stream-error') { + controller.error(new Error('stream failed')); + return; + } + controller.enqueue(new TextEncoder().encode(`data: recording=${span?.isRecording()}\n\n`)); + span?.setAttribute('test.stream.completed', true); + controller.close(); + }, + }); + return new Response(body, { status: 201, headers: { 'content-type': 'text/event-stream' } }); + }, + error() { + return new Response('failed', { status: 500 }); + }, +}); +sendPortToRunner(server.port!); diff --git a/dev-packages/bun-integration-tests/suites/streaming/test.ts b/dev-packages/bun-integration-tests/suites/streaming/test.ts new file mode 100644 index 000000000000..25a0f138a04b --- /dev/null +++ b/dev-packages/bun-integration-tests/suites/streaming/test.ts @@ -0,0 +1,83 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../node-integration-tests/utils/runner'; + +afterAll(cleanupChildProcesses); + +test('records streaming work after the response headers have been returned', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const root = container.items.find(item => item.is_segment); + expect(root).toEqual( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'url.path': { value: '/events', type: 'string' }, + 'http.response.status_code': { value: 201, type: 'integer' }, + 'test.stream.completed': { value: true, type: 'boolean' }, + }), + }), + ); + }, + }) + .start(); + expect(await runner.makeRequest('get', '/events')).toBe('data: first\n\ndata: recording=true\n\n'); + await runner.completed(); +}); + +test('records a thrown handler as an error span', async () => { + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const root = container.items.find(item => item.is_segment); + expect(root).toEqual(expect.objectContaining({ status: 'error' })); + }, + }) + .start(); + await runner.makeRequest('get', '/error', { expectError: true }); + await runner.completed(); +}); + +test('captures a response stream failure with its request context and error span', async () => { + let spanTraceId: string | undefined; + let eventTraceId: string | undefined; + const runner = createRunner(__dirname, 'index.ts') + .withMockSentryServer() + .unordered() + .expect({ + span: container => { + const roots = container.items.filter(item => item.is_segment); + expect(roots).toHaveLength(1); + const root = roots[0]!; + expect(root.status).toBe('error'); + expect(root.attributes['url.path']?.value).toBe('/stream-error'); + expect(root.attributes['http.response.status_code']?.value).toBe(201); + spanTraceId = root.trace_id; + }, + }) + .expect({ + event: event => { + expect(event.exception?.values).toHaveLength(1); + const exception = event.exception!.values![0]!; + expect(exception.type).toBe('Error'); + expect(exception.value).toBe('stream failed'); + expect(exception.mechanism).toEqual({ type: 'auto.http.bun.serve', handled: false }); + expect(event.request?.method).toBe('GET'); + expect(new URL(event.request!.url!).pathname).toBe('/stream-error'); + eventTraceId = event.contexts?.trace?.trace_id; + }, + }) + .start(); + + await expect.poll(() => runner.getPort(), { timeout: 30_000 }).toBeTypeOf('number'); + const response = await fetch(`http://localhost:${runner.getPort()}/stream-error`); + expect(response.status).toBe(201); + // Bun versions differ in whether a failed stream rejects or closes the HTTP response body. + await Promise.allSettled([response.text()]); + await runner.completed(); + expect(spanTraceId).toMatch(/^[a-f\d]{32}$/); + expect(eventTraceId).toBe(spanTraceId); +}); diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index bf73c591d467..1bd340703aec 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -13,13 +13,14 @@ import { isURLObjectRelative, parseStringToURLObject, setHttpStatus, - startSpan, + SPAN_STATUS_ERROR, + startSpanManual, winterCGRequestToRequestData, withIsolationScope, filterCollectedUrl, filterCollectedUrlQuery, } from '@sentry/core'; -import { getClientIPAddress } from '@sentry/core/server'; +import { classifyResponseStreaming, getClientIPAddress } from '@sentry/core/server'; import type { Server, ServeOptions } from 'bun'; import { CLIENT_ADDRESS, @@ -38,6 +39,7 @@ import { SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { HTTP_SERVER } from '@sentry/conventions/op'; +import { monitorStream } from '../utils/streaming'; const INTEGRATION_NAME = 'BunServer' as const; @@ -294,7 +296,7 @@ function wrapRequestHandler( baggage: request.headers.get('baggage'), }, () => - startSpan( + startSpanManual( { attributes: { ...attributes, [SENTRY_OP]: HTTP_SERVER }, // With span streaming, span names have to be low cardinality, so we can't fall back to the URL path. @@ -303,7 +305,7 @@ function wrapRequestHandler( ? `${request.method} ${routeName}` : request.method?.toUpperCase() || HTTP_SPAN_NAME_FALLBACK, }, - async span => { + async (span, endSpan) => { try { const response = (await target.apply(thisArg, args)) as Response | undefined; if (response?.status) { @@ -319,14 +321,28 @@ function wrapRequestHandler( ); } } + if (response?.body && !response.body.locked && classifyResponseStreaming(response).isStreaming) { + const body = monitorStream(response.body, endSpan, error => { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(error, { mechanism: { type: 'auto.http.bun.serve', handled: false } }); + }); + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + endSpan(); return response; } catch (e) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); captureException(e, { mechanism: { type: 'auto.http.bun.serve', handled: false, }, }); + endSpan(); throw e; } }, diff --git a/packages/bun/src/utils/streaming.ts b/packages/bun/src/utils/streaming.ts new file mode 100644 index 000000000000..d19466a6a035 --- /dev/null +++ b/packages/bun/src/utils/streaming.ts @@ -0,0 +1,40 @@ +/** Monitor the source without reading ahead or treating cancellation as a source failure. */ +export function monitorStream( + stream: ReadableStream, + onDone: () => void, + onError: (error: unknown) => void, +): ReadableStream { + const reader = stream.getReader(); + // A cancelled reader closes normally; only a source failure should be captured as an error. + void reader.closed.then(onDone, error => { + onError(error); + onDone(); + }); + + return new ReadableStream( + { + async pull(controller) { + try { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + reader.releaseLock(); + } else { + controller.enqueue(value); + } + } catch (error) { + controller.error(error); + reader.releaseLock(); + } + }, + async cancel(reason) { + try { + await reader.cancel(reason); + } finally { + reader.releaseLock(); + } + }, + }, + { highWaterMark: 0 }, + ); +} diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index a0cb31bed280..0a25b655a892 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -8,9 +8,12 @@ import { instrumentBunServe } from '../../src/integrations/bunserver'; describe('Bun Serve Integration', () => { const mockSpan = SentryCore.startInactiveSpan({ name: 'test span' }); const setAttributesSpy = spyOn(mockSpan, 'setAttributes'); + const setStatusSpy = spyOn(mockSpan, 'setStatus'); + const captureExceptionSpy = spyOn(SentryCore, 'captureException'); const continueTraceSpy = spyOn(SentryCore, 'continueTrace'); - const startSpanSpy = spyOn(SentryCore, 'startSpan').mockImplementation((_opts, cb) => { - return cb(mockSpan as unknown as SentryCore.Span); + const endSpanSpy = spyOn(mockSpan, 'end'); + const startSpanSpy = spyOn(SentryCore, 'startSpanManual').mockImplementation((_opts, cb) => { + return cb(mockSpan as unknown as SentryCore.Span, () => mockSpan.end()); }); const setupClient = (options?: BunOptions): void => { @@ -30,8 +33,11 @@ describe('Bun Serve Integration', () => { beforeEach(() => { startSpanSpy.mockClear(); + endSpanSpy.mockReset(); continueTraceSpy.mockClear(); setAttributesSpy.mockClear(); + setStatusSpy.mockClear(); + captureExceptionSpy.mockClear(); // Header attributes are only collected while a client is active, so every test sets up its own instead of // relying on one leaking in from whichever test file `bun test` happened to run first. setupClient(); @@ -47,6 +53,149 @@ describe('Bun Serve Integration', () => { port += 1; }); + test.each(['fetch', 'route'])( + 'keeps a streaming %s response span open until the last chunk is consumed', + async mode => { + let controller: ReadableStreamDefaultController; + const source = new ReadableStream({ + start(value) { + controller = value; + }, + }); + const ended = new Promise(resolve => endSpanSpy.mockImplementation(() => resolve())); + const handler = () => + new Response(source, { status: 201, headers: { 'content-type': 'text/event-stream', 'x-stream': 'events' } }); + const server = Bun.serve({ + port, + ...(mode === 'fetch' ? { fetch: handler } : { routes: { '/events': { GET: handler } } }), + }); + try { + controller!.enqueue(new TextEncoder().encode('data: first\n\n')); + const response = await fetch(`http://localhost:${port}/events`); + const reader = response.body!.getReader(); + expect(response.status).toBe(201); + expect(response.headers.get('x-stream')).toBe('events'); + expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n'); + expect(endSpanSpy).toHaveBeenCalledTimes(0); + + controller!.enqueue(new TextEncoder().encode('data: last\n\n')); + controller!.close(); + expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: last\n\n'); + expect((await reader.read()).done).toBe(true); + await ended; + expect(endSpanSpy).toHaveBeenCalledTimes(1); + } finally { + await server.stop(true); + } + }, + ); + + test.each(['client disconnected', new Error('client disconnected')])( + 'ends the span without capturing a cancelled stream: %s', + async reason => { + let cancelled: unknown; + const ended = new Promise(resolve => endSpanSpy.mockImplementation(() => resolve())); + const source = new ReadableStream({ + cancel(reason) { + cancelled = reason; + }, + }); + const server = Bun.serve({ + port, + fetch: () => new Response(source, { headers: { 'content-type': 'text/event-stream' } }), + }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/events`)); + const reader = response.body!.getReader(); + const pendingRead = reader.read(); + await reader.cancel(reason); + expect(await pendingRead).toEqual({ done: true, value: undefined }); + await ended; + expect(cancelled).toBe(reason); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(setStatusSpy).not.toHaveBeenCalledWith({ + code: SentryCore.SPAN_STATUS_ERROR, + message: 'internal_error', + }); + } finally { + await server.stop(true); + } + }, + ); + + test('preserves backpressure instead of reading ahead of the response consumer', async () => { + let pulls = 0; + const source = new ReadableStream( + { + pull(controller) { + pulls += 1; + controller.enqueue(new TextEncoder().encode(`data: ${pulls}\n\n`)); + }, + }, + { highWaterMark: 0 }, + ); + const server = Bun.serve({ + port, + fetch: () => new Response(source, { headers: { 'content-type': 'text/event-stream' } }), + }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/events`)); + expect(pulls).toBe(0); + const reader = response.body!.getReader(); + expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: 1\n\n'); + expect(pulls).toBe(1); + expect(endSpanSpy).not.toHaveBeenCalled(); + + await reader.cancel(); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + } finally { + await server.stop(true); + } + }); + + test('captures a response stream error and marks its span as failed', async () => { + let controller: ReadableStreamDefaultController; + const ended = new Promise(resolve => endSpanSpy.mockImplementation(() => resolve())); + const source = new ReadableStream({ + start(value) { + controller = value; + }, + }); + const server = Bun.serve({ + port, + fetch: () => new Response(source, { headers: { 'content-type': 'application/x-ndjson' } }), + }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/events`)); + const error = new Error('stream interrupted'); + controller!.error(error); + await ended; + await expect(response.text()).rejects.toBe(error); + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.bun.serve', handled: false }, + }); + expect(setStatusSpy).toHaveBeenCalledWith({ code: SentryCore.SPAN_STATUS_ERROR, message: 'internal_error' }); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + } finally { + await server.stop(true); + } + }); + + test('ends a non-streaming response span before the body is consumed', async () => { + const server = Bun.serve({ port, fetch: () => Response.json({ ok: true }) }); + try { + const response = await server.fetch(new Request(`http://localhost:${port}/status`)); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + expect(await response.json()).toEqual({ ok: true }); + expect(endSpanSpy).toHaveBeenCalledTimes(1); + } finally { + await server.stop(true); + } + }); + test('generates a transaction around a request', async () => { const server = Bun.serve({ async fetch(_req) { @@ -372,6 +521,7 @@ describe('Bun Serve Integration', () => { expect(await initialResponse.text()).toBe('Initial handler'); expect(startSpanSpy).toHaveBeenCalledTimes(1); startSpanSpy.mockClear(); + endSpanSpy.mockReset(); // Reload server with new handler server.reload({ diff --git a/packages/cloudflare/src/wrapRequestHandlerWithInit.ts b/packages/cloudflare/src/wrapRequestHandlerWithInit.ts index 18f2ce63ddb2..3599b88f4243 100644 --- a/packages/cloudflare/src/wrapRequestHandlerWithInit.ts +++ b/packages/cloudflare/src/wrapRequestHandlerWithInit.ts @@ -18,6 +18,7 @@ import { startSpanManual, winterCGHeadersToDict, } from '@sentry/core'; +import { classifyResponseStreaming } from '@sentry/core/server'; import { captureIncomingRequestBody } from './integrations/httpServer'; import { flushDeferredChannelEvents } from './orchestrion-deferred-channels'; import type { CloudflareClient, CloudflareOptions } from './client'; @@ -26,7 +27,6 @@ import { flushAndDispose, getOriginalWaitUntil } from './flush'; import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; import { getInvocationState, getInvocationWaitUntil } from './utils/invocationContext'; import { withInvocationIsolationScope } from './utils/invocationScope'; -import { classifyResponseStreaming } from './utils/streaming'; function getRequestErrorMechanismType(context: ExecutionContextCompat | undefined): string { // Durable Object fetch handlers use DO state as context (see instrumentDurableObjectWithSentry) diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index a34657db8d6a..e44bb9c2df8b 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -41,3 +41,5 @@ export type { } from './integrations/http/types'; export { createFetchIntegration } from './integrations/fetch'; export type { FetchIntegrationOptions } from './integrations/fetch'; +export { classifyResponseStreaming } from './utils/responseStreaming'; +export type { StreamingGuess } from './utils/responseStreaming'; diff --git a/packages/cloudflare/src/utils/streaming.ts b/packages/core/src/utils/responseStreaming.ts similarity index 100% rename from packages/cloudflare/src/utils/streaming.ts rename to packages/core/src/utils/responseStreaming.ts diff --git a/packages/core/test/lib/utils/responseStreaming.test.ts b/packages/core/test/lib/utils/responseStreaming.test.ts new file mode 100644 index 000000000000..5ec7779bd7e8 --- /dev/null +++ b/packages/core/test/lib/utils/responseStreaming.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'vitest'; +import { classifyResponseStreaming } from '../../../src/utils/responseStreaming'; + +describe('classifyResponseStreaming', () => { + test.each([ + ['text/event-stream; charset=utf-8', true], + ['application/x-ndjson', true], + ['application/ndjson', true], + ['application/stream+json', true], + ['TEXT/EVENT-STREAM', true], + ['text/plain', true], + ['text/html', false], + ['application/json', false], + ['application/octet-stream', false], + ])('classifies %s without reading the body', (contentType, isStreaming) => { + const response = new Response(new ReadableStream(), { headers: { 'content-type': contentType } }); + + expect(classifyResponseStreaming(response)).toEqual({ isStreaming }); + expect(response.bodyUsed).toBe(false); + expect(response.body?.locked).toBe(false); + }); + + test('treats plain text with a known length as non-streaming', () => { + const response = new Response('ready', { headers: { 'content-type': 'text/plain', 'content-length': '5' } }); + + expect(classifyResponseStreaming(response)).toEqual({ isStreaming: false }); + }); + + test('treats a bodyless response as non-streaming even with streaming headers', () => { + const response = new Response(null, { status: 204, headers: { 'content-type': 'text/event-stream' } }); + + expect(classifyResponseStreaming(response)).toEqual({ isStreaming: false }); + }); +}); diff --git a/packages/deno/src/utils/streaming.ts b/packages/deno/src/utils/streaming.ts index 491131e6e093..ca6f778b749e 100644 --- a/packages/deno/src/utils/streaming.ts +++ b/packages/deno/src/utils/streaming.ts @@ -1,46 +1,5 @@ import type { Span } from '@sentry/core'; - -export type StreamingGuess = { - isStreaming: boolean; -}; - -/** - * Classifies a Response as streaming or non-streaming. - * - * Heuristics: - * - No body → not streaming - * - Known streaming Content-Types → streaming (SSE, NDJSON, JSON streaming) - * - text/plain without Content-Length → streaming (some AI APIs) - * - Otherwise → not streaming (conservative default, including HTML/SSR) - * - * We avoid probing the stream to prevent blocking on transform streams (like injectTraceMetaTags) - * or SSR streams that may not have data ready immediately. - */ -export function classifyResponseStreaming(res: Response): StreamingGuess { - if (!res.body) { - return { isStreaming: false }; - } - - const contentType = res.headers.get('content-type') ?? ''; - const contentLength = res.headers.get('content-length'); - - // Streaming: Known streaming content types - // - text/event-stream: Server-Sent Events (Vercel AI SDK, real-time APIs) - // - application/x-ndjson, application/ndjson: Newline-delimited JSON - // - application/stream+json: JSON streaming - // - text/plain (without Content-Length): Some AI APIs use this for streaming text - if ( - /^text\/event-stream\b/i.test(contentType) || - /^application\/(x-)?ndjson\b/i.test(contentType) || - /^application\/stream\+json\b/i.test(contentType) || - (/^text\/plain\b/i.test(contentType) && !contentLength) - ) { - return { isStreaming: true }; - } - - // Default: treat as non-streaming - return { isStreaming: false }; -} +import { classifyResponseStreaming } from '@sentry/core/server'; /** * Tee a stream, and end the provided span when the stream ends. From 456540823cc982dc994d9143daecd285a7aa3584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:22:23 +0200 Subject: [PATCH 62/73] feat(deps): Bump svgo from 4.0.2 to 4.1.0 (#24220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [svgo](https://github.com/svg/svgo) from 4.0.2 to 4.1.0.
Release notes

Sourced from svgo's releases.

v4.1.0

This minor release upgrades the SAX parser and introduces stricter XML validation. It also includes important security hardening for removeScripts, dependency updates, and improvements to the test and regression infrastructure.

Support SVGO

If SVGO is valuable to you or your organization, please consider supporting the project on OpenCollective. Your sponsorship helps fund ongoing maintenance and security work.

Stricter XML validation

SVGO now uses sax 1.6.1, upgraded from 1.5.0 (#2257).

The new parser version validates numeric character references against the ranges permitted by XML. Invalid references are now rejected in both text and attributes, including:

  • disallowed control characters such as &[#1](https://github.com/svg/svgo/issues/1);, &#xB;, and &#x1F;;
  • UTF-16 surrogate code points such as &#xD800;;
  • invalid XML code points such as &#xFFFF;.

Valid boundary values—including U+0020, U+D7FF, U+E000, U+FFFD, and characters through U+10FFFF—remain supported.

Parser failures are consistently exposed as SvgoParserError errors with an Invalid character entity reason.

This is an intentional behavior change: malformed SVGs that were previously accepted may now produce a parser error, while valid XML documents are unaffected.

Security

The removeScripts plugin has been hardened against several script-execution bypasses:

  • Filters executable data: URLs containing HTML, XHTML, or SVG documents while preserving inert data such as PNG images, and filters legacy vbscript: URLs (#2263).
  • Sanitizes content inside SVG <foreignObject> elements by removing HTML event-handler attributes, srcdoc, and executable URLs from action, data, formaction, href, and src, while preserving non-executable HTML and visual content (#2264).
  • Recognizes namespace-prefixed SVG <a> elements and removes ASCII tabs and newlines before checking URL schemes, preventing values such as java&[#9](https://github.com/svg/svgo/issues/9);script: from bypassing detection while preserving elements in unrelated custom namespaces (#2268).

These changes address:

Dependencies

  • Upgraded css-select to v6 and css-what to v7, and updated SVGO's custom selector adapter for css-select v6 (#2244).

Project maintenance

@​TrySound is back as an active SVGO maintainer.

Many thanks to @​KTibow, @​SethFalco, and @​XhmikosR for maintaining and improving SVGO over the past several years.

Full Changelog: https://github.com/svg/svgo/compare/v4.0.2...v4.1.0

Commits
  • 5765cbe chore: prepare v4.1.0 release (#2275)
  • 3db3ef3 fix(removeScripts): handle anchor URL bypasses (#2268)
  • 4e9b9ae chore: cache regression screenshots (#2267)
  • d55270c chore(regression): migrate comparison workers to Tinypool (#2266)
  • fd51e47 fix(removeScripts): sanitize foreignObject content (#2264)
  • dcaf957 chore: optimize fixtures in a bounded worker pool (#2265)
  • a354293 fix(removeScripts): filter executable data URLs (#2263)
  • 4e0d2ac ci(typecheck): return typechecking on CI
  • 0b97fed test(typescript): drop tsd for vitest type testing API
  • f6e8ae1 chore(pnpm): drop package.json#pnpm.onlyBuiltDependencies
  • Additional commits viewable in compare view
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for svgo since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=svgo&package-manager=npm_and_yarn&previous-version=4.0.2&new-version=4.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrei Borza --- yarn.lock | 53 +++++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0db76c2a23fb..c07d28325713 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13364,16 +13364,16 @@ css-select@^4.1.3, css-select@^4.2.0: domutils "^2.8.0" nth-check "^2.0.1" -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== +css-select@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-6.0.0.tgz#7e63f09881ad118084091048ed543786dad96644" + integrity sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw== dependencies: boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" + css-what "^7.0.0" + domhandler "^5.0.3" + domutils "^3.2.2" + nth-check "^2.1.1" css-tree@^2.3.1: version "2.3.1" @@ -13399,11 +13399,16 @@ css-tree@~2.2.0: mdn-data "2.0.28" source-map-js "^1.0.1" -css-what@^6.0.1, css-what@^6.1.0: +css-what@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +css-what@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-7.0.0.tgz#5796fbebd43571d73c60ba0dd7a6e75dd0d22fe4" + integrity sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ== + css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -14056,10 +14061,10 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" -domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== +domutils@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" @@ -20964,7 +20969,7 @@ npmlog@^6.0.0: gauge "^4.0.3" set-blocking "^2.0.0" -nth-check@^2.0.1: +nth-check@^2.0.1, nth-check@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== @@ -24574,10 +24579,10 @@ sass@^1.49.9: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sax@^1.2.4, sax@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.5.0.tgz#b5549b671069b7aa392df55ec7574cf411179eb8" - integrity sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA== +sax@1.6.1, sax@^1.2.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.1.tgz#4c23cf608c0b693ab54b4b5888e92cfe977b9843" + integrity sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q== sax@~1.2.4: version "1.2.4" @@ -25907,17 +25912,17 @@ svelte@^4.2.8: periscopic "^3.1.0" svgo@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-4.0.2.tgz#a62246f0a9d671c0314d04f3cc15f78b1bd0667f" - integrity sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng== + version "4.1.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-4.1.0.tgz#c977eb69640ef232a5e8236a5a327ed8c9d52853" + integrity sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q== dependencies: commander "^11.1.0" - css-select "^5.1.0" + css-select "^6.0.0" css-tree "^3.0.1" - css-what "^6.1.0" + css-what "^7.0.0" csso "^5.0.5" picocolors "^1.1.1" - sax "^1.5.0" + sax "1.6.1" swr@^2.2.5: version "2.2.5" From 0369ca9d6abf27fe6d8f74d516c8b379f536d2dd Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:26:57 +0200 Subject: [PATCH 63/73] chore: Add external contributor to CHANGELOG.md (#25111) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #25025 Co-authored-by: JPeer264 <10677263+JPeer264@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ff401eb033..1b14891dde78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @minwookshin. Thank you for your contribution! + ## 11.4.0 - feat(sveltekit): Support stable SvelteKit 3 ([#25009](https://github.com/getsentry/sentry-javascript/pull/25009)) From 2cc2b29250cb7dc24afd7afdf2a85ee0425ed99d Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Tue, 6 Oct 2026 18:37:15 +0300 Subject: [PATCH 64/73] fix(server-utils): Derive the Vercel AI conversation id from the OpenAI conversation option (#24979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vercel AI integration filled `gen_ai.conversation.id` with `providerMetadata.openai.responseId`. That is the id of the response that just came back, so every call got a different value under a grouping attribute and showed up as its own one-span conversation. The same value is already on `gen_ai.response.id`. The mapping came in with #16992 and #19903 later had to guard it from overwriting user-set ids. The id now comes from `providerOptions.openai.conversation` (or `azure`), the Conversations API `conv_` id, which is the same on every turn. Child model-call and tool spans inherit it from the active operation span. `Sentry.setConversationId()` still wins, since `conversationIdIntegration` writes the scope value on `spanStart` after these start attributes. The v6 adapter forwards `providerOptions` so `ai` 4 to 6 behave the same. Not in this PR: reading the id from `runtimeContext` / `experimental_telemetry.metadata` goes with #24706 once #24721 lands. Part of #24832 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 --- .../suites/tracing/vercelai/test.ts | 2 +- .../v6_v7/scenario-openai-conversation.mjs | 75 +++++++++ .../suites/tracing/vercelai/v6_v7/test.ts | 71 +++++++- .../server-utils/src/ai/vercel-ai/index.ts | 6 +- .../vercel-ai/vercel-ai-dc-subscriber.ts | 55 +++++-- .../vercel-ai-orchestrion-subscriber.ts | 3 + .../vercel-ai/conversation-id.test.ts | 151 ++++++++++++++++++ 7 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs create mode 100644 packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index 75945701e92f..e6ec70fa35ff 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -492,7 +492,7 @@ describe('Vercel AI integration (v4)', () => { }); createEsmAndCjsTests(__dirname, 'scenario-conversation-id.mjs', 'instrument.mjs', (createRunner, test) => { - test('does not overwrite conversation id set via Sentry.setConversationId with responseId from provider metadata', async () => { + test('keeps the conversation id set via Sentry.setConversationId and ignores the provider responseId', async () => { await createRunner() .expect({ transaction: { transaction: 'main' } }) .expect({ diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs new file mode 100644 index 000000000000..fd724db00276 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-openai-conversation.mjs @@ -0,0 +1,75 @@ +import * as Sentry from '@sentry/node'; +import { generateText, tool } from 'ai'; +import { MockLanguageModelV3 } from 'ai/test'; +import { z } from 'zod'; + +const usage = { + inputTokens: { total: 10, noCache: 10, cached: 0 }, + outputTokens: { total: 5, noCache: 5, cached: 0 }, + totalTokens: { total: 15, noCache: 15, cached: 0 }, +}; + +const textModel = new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage, + content: [{ type: 'text', text: 'Hello!' }], + warnings: [], + // A per-response id: present on every turn, never the conversation id. + providerMetadata: { openai: { responseId: 'resp_turn' } }, + }), +}); + +const toolCallModel = new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'tool-calls', raw: 'tool_calls' }, + usage, + content: [{ type: 'tool-call', toolCallId: 'tc-1', toolName: 'echo', input: JSON.stringify({ text: 'hi' }) }], + warnings: [], + }), +}); + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + // A turn of an OpenAI Conversations API conversation. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: textModel, + prompt: 'First turn', + providerOptions: { openai: { conversation: 'conv_abc123' } }, + }); + + // The Azure Responses API carries the same option under the `azure` key; this turn also runs a tool. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: toolCallModel, + prompt: 'Second turn', + providerOptions: { azure: { conversation: 'conv_azure' } }, + tools: { + echo: tool({ + inputSchema: z.object({ text: z.string() }), + execute: async ({ text }) => text, + }), + }, + }); + + // Chaining on the previous response names a response, not a thread, so no conversation id. + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: textModel, + prompt: 'Chained turn', + providerOptions: { openai: { previousResponseId: 'resp_turn' } }, + }); + + // An id set through the SDK API wins over the provider option. Last, since it stays on the scope. + Sentry.setConversationId('conv-from-api'); + await generateText({ + experimental_telemetry: { isEnabled: true }, + model: textModel, + prompt: 'API turn', + providerOptions: { openai: { conversation: 'conv_ignored' } }, + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index f8a26a410833..805a65313a66 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -767,7 +767,7 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe 'scenario-provider-metadata.mjs', 'instrument.mjs', (createRunner, test) => { - test('derives provider-metadata token breakdown, conversation id and system instructions', async () => { + test('derives provider-metadata token breakdown and system instructions', async () => { await createRunner() .expect({ transaction: { transaction: 'main' } }) .expect({ @@ -781,12 +781,13 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe )!; expect(generateContent).toBeDefined(); - // Cache/reasoning token breakdown and conversation id are derived from the model's - // `providerMetadata` — by the OTel processor on v6 and by the channel subscriber on v7, - // both via the shared `getProviderMetadataAttributes` helper, so the shape is identical. + // Cache/reasoning token breakdown is derived from the model's `providerMetadata` by the + // channel subscriber, which v6 reaches through the orchestrion adapter, so the shape is the + // same on both versions. expect(generateContent.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]?.value).toBe(5); expect(generateContent.attributes[GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]?.value).toBe(7); - expect(generateContent.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe('resp_abc123'); + // The per-response `responseId` is not a conversation id and must not be recorded as one. + expect(generateContent.attributes[GEN_AI_CONVERSATION_ID]).toBeUndefined(); const invokeAgent = container.items.find( span => span.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', @@ -813,6 +814,66 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe }, ); + createEsmTests( + __dirname, + 'scenario-openai-conversation.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('derives gen_ai.conversation.id from the OpenAI `conversation` provider option', async () => { + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + const genAiSpans = container.items.filter(s => + String(s.attributes['sentry.op']?.value ?? '').startsWith('gen_ai.'), + ); + const conversationIdOf = (span: (typeof genAiSpans)[number]) => + span.attributes[GEN_AI_CONVERSATION_ID]?.value; + const invokeAgentSpans = genAiSpans.filter( + s => s.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', + ); + expect(invokeAgentSpans).toHaveLength(4); + const [firstTurn, secondTurn, chainedTurn, apiTurn] = invokeAgentSpans.sort( + (a, b) => a.start_timestamp - b.start_timestamp, + ); + + // `providerOptions.openai.conversation` is the Conversations API id: the same on every turn. + expect(conversationIdOf(firstTurn!)).toBe('conv_abc123'); + // The Azure Responses API uses the `azure` key for the same option. + expect(conversationIdOf(secondTurn!)).toBe('conv_azure'); + // `previousResponseId` names a response rather than a thread, and the response's own + // `responseId` is recorded as `gen_ai.response.id` only. + expect(conversationIdOf(chainedTurn!)).toBeUndefined(); + // `Sentry.setConversationId()` beats the provider option. + expect(conversationIdOf(apiTurn!)).toBe('conv-from-api'); + + // Model-call and tool spans carry their operation's id, even though their start events + // do not carry `providerOptions`. + const modelCallSpans = genAiSpans.filter( + s => s.attributes['sentry.op']?.value === 'gen_ai.generate_content', + ); + expect(modelCallSpans.map(conversationIdOf).sort()).toEqual([ + 'conv-from-api', + 'conv_abc123', + 'conv_azure', + undefined, + ]); + const toolSpan = genAiSpans.find(s => s.attributes['sentry.op']?.value === 'gen_ai.execute_tool')!; + expect(toolSpan).toBeDefined(); + expect(conversationIdOf(toolSpan)).toBe('conv_azure'); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + ai: vercelAiVersion, + }, + }, + ); + createEsmTests( __dirname, 'scenario-cache-tokens.mjs', diff --git a/packages/server-utils/src/ai/vercel-ai/index.ts b/packages/server-utils/src/ai/vercel-ai/index.ts index 76a355fdc588..3296eb18a73f 100644 --- a/packages/server-utils/src/ai/vercel-ai/index.ts +++ b/packages/server-utils/src/ai/vercel-ai/index.ts @@ -1,6 +1,5 @@ import type { SpanAttributeValue } from '@sentry/core'; import { - GEN_AI_CONVERSATION_ID, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, @@ -10,8 +9,8 @@ import { import type { OpenAiProviderMetadata, ProviderMetadata } from './vercel-ai-attributes'; /** - * Derive the `gen_ai.usage.*` cache/reasoning/prediction token attributes and `gen_ai.conversation.id` - * from an AI SDK `providerMetadata` object. + * Derive the `gen_ai.usage.*` cache/reasoning/prediction token attributes from an AI SDK + * `providerMetadata` object. * * Used by the `ai` >= 7 tracing-channel subscriber, which receives `providerMetadata` as an object on * the channel result. Pass the already-parsed object; unknown/empty input yields `{}`. @@ -39,7 +38,6 @@ export function getProviderMetadataAttributes(providerMetadata: unknown): Record 'gen_ai.usage.output_tokens.prediction_rejected', openaiMetadata.rejectedPredictionTokens, ); - setAttributeIfDefined(attributes, GEN_AI_CONVERSATION_ID, openaiMetadata.responseId); } if (metadata.anthropic) { diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts index 4daec0d0edf7..e9a75ade26b6 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts @@ -39,6 +39,7 @@ import type { Span, SpanAttributes } from '@sentry/core'; import { _INTERNAL_skipAiProviderWrapping, captureException, + getActiveSpan, getClient, isObjectLike, SPAN_STATUS_ERROR, @@ -141,6 +142,31 @@ export function clearOperationCallId(callId: string): void { invokeAgentSpanByCallId.delete(callId); } +/** + * The OpenAI Conversations API id from `providerOptions.openai.conversation` (or `azure`): the one + * provider-level value that is the same on every turn. A `Sentry.setConversationId()` value still wins, + * since `conversationIdIntegration` writes it on `spanStart`, after these start attributes. + */ +function getOpenAiConversationId(providerOptions: unknown): string | undefined { + if (!isObjectLike(providerOptions)) { + return undefined; + } + for (const key of ['openai', 'azure']) { + const options = providerOptions[key]; + const conversation = isObjectLike(options) ? asString(options.conversation) : undefined; + if (conversation) { + return conversation; + } + } + return undefined; +} + +/** The `gen_ai.conversation.id` already on the active span, which for a child event is its operation span. */ +function getActiveSpanConversationId(): string | undefined { + const active = getActiveSpan(); + return active ? asString(spanToJSON(active).attributes[GEN_AI_CONVERSATION_ID]) : undefined; +} + /** * `providerMetadata` is last-step only; drop derived usage on spans that report an aggregate. * @@ -417,12 +443,20 @@ export function createSpanFromMessage( recordToolDescriptions(callId, event.tools); } + // Only an operation's start event carries `providerOptions`; its model-call and tool events start + // while the operation span is active, so they inherit the id from it. A root operation never + // inherits, so a nested call (e.g. inside a tool's `execute`) is not folded into the outer conversation. + const conversationId = + getOpenAiConversationId(event.providerOptions) ?? + (ROOT_OPERATION_TYPES.has(type) ? undefined : getActiveSpanConversationId()); + const baseAttributes: SpanAttributes = { [SENTRY_ORIGIN]: ORIGIN, ...telemetryMetadataAttributes(event.telemetryMetadata), ...(provider ? { [GEN_AI_PROVIDER_NAME]: provider, [VERCEL_AI_MODEL_PROVIDER_ATTRIBUTE]: provider } : {}), ...(modelId ? { [GEN_AI_REQUEST_MODEL]: modelId } : {}), ...(maxRetries !== undefined ? { [VERCEL_AI_SETTINGS_MAX_RETRIES_ATTRIBUTE]: maxRetries } : {}), + ...(conversationId ? { [GEN_AI_CONVERSATION_ID]: conversationId } : {}), }; switch (type) { @@ -438,7 +472,7 @@ export function createSpanFromMessage( return buildModelCallSpan(event, baseAttributes, recordInputs, callId, modelId); case 'executeTool': - return buildToolSpan(event, recordInputs); + return buildToolSpan(event, recordInputs, conversationId); case 'embed': case 'embedMany': { // `embed` carries a single `value`; `embedMany` a `values` array — both map to the embeddings input. @@ -542,7 +576,11 @@ function buildModelCallSpan( }); } -function buildToolSpan(event: Record, recordInputs: boolean): Span { +function buildToolSpan( + event: Record, + recordInputs: boolean, + conversationId: string | undefined, +): Span { const toolCall = isObjectLike(event.toolCall) ? event.toolCall : {}; const toolName = asString(toolCall.toolName); const toolCallId = asString(event.toolCallId) ?? asString(toolCall.toolCallId); @@ -557,6 +595,7 @@ function buildToolSpan(event: Record, recordInputs: boolean): S ...(toolCallId ? { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: toolCallId } : {}), ...(description ? { [GEN_AI_TOOL_DESCRIPTION]: description } : {}), ...(recordInputs && toolInput !== undefined ? { [GEN_AI_TOOL_CALL_ARGUMENTS]: stringify(toolInput) } : {}), + ...(conversationId ? { [GEN_AI_CONVERSATION_ID]: conversationId } : {}), }); } @@ -628,17 +667,11 @@ export function enrichSpanOnEnd( span.setAttribute(GEN_AI_RESPONSE_MODEL, responseModel); } - // Provider-specific cache/reasoning/prediction token breakdowns and `gen_ai.conversation.id`. - // The channel exposes `providerMetadata` as an object (the OTel path parses it from a string); - // both share `getProviderMetadataAttributes` so the emitted shape is identical. + // Provider-specific cache/reasoning/prediction token breakdowns. The channel exposes `providerMetadata` + // as an object (the OTel path parses it from a string); both share `getProviderMetadataAttributes` so + // the emitted shape is identical. const providerMetadata = (result as { providerMetadata?: unknown }).providerMetadata; const providerAttributes = getProviderMetadataAttributes(providerMetadata); - // Don't overwrite a conversation id already set on span start (e.g. by `conversationIdIntegration` - // from a user-set scope value); the provider-derived id is only a fallback. Matches the OTel path. - if (GEN_AI_CONVERSATION_ID in providerAttributes && spanToJSON(span).attributes[GEN_AI_CONVERSATION_ID]) { - // oxlint-disable-next-line typescript/no-dynamic-delete - delete providerAttributes[GEN_AI_CONVERSATION_ID]; - } dropLastStepOnlyUsage(providerAttributes, type); span.setAttributes(providerAttributes); diff --git a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts index 40f076f54bec..daf4ee24584c 100644 --- a/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts +++ b/packages/server-utils/src/integrations/vercel-ai/vercel-ai-orchestrion-subscriber.ts @@ -676,6 +676,9 @@ function buildTextMessage(type: 'generateText' | 'streamText' | 'generateObject' // Normalize to the message-array shape the shared core (and v7's channel) expects: a bare string // `prompt` becomes a single user message, matching the SDK's own normalization. messages: normalizePromptMessages(options), + // v7's native start event carries `providerOptions`; the shared core reads the OpenAI + // Conversations API id from it. + providerOptions: options.providerOptions, telemetryMetadata: telemetry.metadata, ...recording(telemetry), }, diff --git a/packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts b/packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts new file mode 100644 index 000000000000..07b5a35da9f5 --- /dev/null +++ b/packages/server-utils/test/integrations/vercel-ai/conversation-id.test.ts @@ -0,0 +1,151 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { GEN_AI_CONVERSATION_ID, GEN_AI_RESPONSE_ID } from '@sentry/conventions/attributes'; +import { + conversationIdIntegration, + getMainCarrier, + setConversationId, + setCurrentClient, + spanToStaticSpanJSON, + withActiveSpan, +} from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { getProviderMetadataAttributes } from '../../../src/ai/vercel-ai'; +import { + clearOperationCallId, + createSpanFromMessage, + enrichSpanOnEnd, +} from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; + +type Message = Parameters[0]; +const channelOptions = {} as Parameters[1]; + +describe('Vercel AI SDK conversation id', () => { + let endedSpans: Span[]; + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + // The bare test client has no default integrations; the Node SDK registers this one itself. + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [conversationIdIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + endedSpans = []; + client.on('spanEnd', span => endedSpans.push(span)); + }); + + afterEach(() => { + setConversationId(undefined); + clearOperationCallId('call-1'); + getMainCarrier().__SENTRY__ = undefined; + }); + + function run(message: Message): Record { + const span = createSpanFromMessage(message, channelOptions)!; + enrichSpanOnEnd(span, message, {} as Parameters[2]); + span.end(); + return spanToStaticSpanJSON(endedSpans[endedSpans.length - 1]!).data ?? {}; + } + + it('reads the OpenAI Conversations API id from the operation `providerOptions`', () => { + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + }); + + it('reads the id from the `azure` provider options', () => { + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { azure: { conversation: 'conv_azure' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv_azure'); + }); + + it('reads the `azure` id when the `openai` options have no conversation', () => { + const data = run({ + type: 'generateText', + event: { + callId: 'call-1', + providerOptions: { openai: { reasoningEffort: 'low' }, azure: { conversation: 'conv_azure' } }, + }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv_azure'); + }); + + it('passes the operation id on to the model-call and tool spans that start under it', () => { + const operationMessage: Message = { + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }; + const operationSpan = createSpanFromMessage(operationMessage, channelOptions)!; + // Child events start while the operation span is the active span (the channel binds it as context). + const [modelCall, toolCall] = withActiveSpan(operationSpan, () => [ + run({ type: 'languageModelCall', event: { callId: 'call-1', modelId: 'gpt-4' } }), + run({ + type: 'executeTool', + event: { callId: 'call-1', toolCall: { toolName: 'echo', toolCallId: 'tc-1', input: {} } }, + }), + ]); + operationSpan.end(); + + expect(spanToStaticSpanJSON(operationSpan).data?.[GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + expect(modelCall![GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + expect(toolCall![GEN_AI_CONVERSATION_ID]).toBe('conv_abc'); + }); + + it('does not pass an outer conversation id to a nested operation', () => { + const outer = createSpanFromMessage( + { + type: 'generateText', + event: { callId: 'call-outer', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }, + channelOptions, + )!; + // e.g. a sub-agent `generateText` called from inside a tool's `execute` + const inner = withActiveSpan(outer, () => + run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { previousResponseId: 'resp_1' } } }, + }), + ); + outer.end(); + clearOperationCallId('call-outer'); + + expect(inner[GEN_AI_CONVERSATION_ID]).toBeUndefined(); + }); + + it('sets nothing for `previousResponseId` chaining, which names a response rather than a thread', () => { + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { previousResponseId: 'resp_1' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBeUndefined(); + }); + + it('does not record the provider `responseId` as the conversation id', () => { + const data = run({ + type: 'languageModelCall', + event: { callId: 'call-1', modelId: 'gpt-4' }, + result: { response: { id: 'resp_1' }, providerMetadata: { openai: { responseId: 'resp_1' } } }, + }); + expect(data[GEN_AI_RESPONSE_ID]).toBe('resp_1'); + expect(data[GEN_AI_CONVERSATION_ID]).toBeUndefined(); + expect(getProviderMetadataAttributes({ openai: { responseId: 'resp_1' } })).toEqual({}); + }); + + it('lets an id set via `Sentry.setConversationId()` win over the provider option', () => { + setConversationId('conv-from-scope'); + const data = run({ + type: 'generateText', + event: { callId: 'call-1', providerOptions: { openai: { conversation: 'conv_abc' } } }, + }); + expect(data[GEN_AI_CONVERSATION_ID]).toBe('conv-from-scope'); + }); +}); From 40ad4ca5988f57634850a4290f08559ce0502ebf Mon Sep 17 00:00:00 2001 From: Rola Abuhasna Date: Tue, 6 Oct 2026 18:40:16 +0300 Subject: [PATCH 65/73] feat(langchain): Derive gen_ai.conversation.id from the invoke config (#24831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LangChain integration only set `gen_ai.conversation.id` when the user called `Sentry.setConversationId()` themselves. LangGraph read `configurable.thread_id`, but stamped it on its own `invoke_agent` span only, so the chat and tool child spans went out without it. Child runs inherit their parent's metadata, so this PR reads the conversation id from run metadata and sets the attribute on the chat, chain and tool spans. On `@langchain/core` 0.1.20 to 1.1.39, `ensureConfig` copies every primitive `config.configurable` entry into that metadata, so an id passed to `invoke()` is already there. `@langchain/core` 1.1.40 stopped doing that for any handler except LangSmith's tracer. So where we inject our handler, in the chat-model hook and the LangGraph `invoke` proxy, we copy the three keys below from `configurable` into `metadata` ourselves. Metadata the user set takes precedence. Keys read, in order: - `thread_id`, the LangGraph checkpointer key and one of the two keys LangSmith groups threads by - `session_id`, the other LangSmith key - `sessionId`, what `RunnableWithMessageHistory` requires in JS An id set on the scope via `Sentry.setConversationId()` still wins. `conversationIdIntegration` applies it on `spanStart`, after the initial attributes, so an explicit user id overwrites the derived one. The integration test covers this. LangGraph's `invoke_agent` span and the tool spans `createReactAgent` creates now use the same helper. So they read the same keys, accept numeric ids, and let an id from `Sentry.setConversationId()` win over `thread_id`. Before, `thread_id` overwrote the scope id on the `invoke_agent` span only, and the tool spans never picked up `thread_id`. One gap remains on `@langchain/core` >= 1.1.40. If you pass `createLangChainCallbackHandler()` yourself to a chain outside LangGraph, the chain and tool spans get no id from `configurable`, because the handler never sees it. The chat spans still get it. Closing that would mean hooking more than chat models, so it is left for a follow-up. **Bun.** The new suite failed on the Bun runner because `@sentry/bun` builds its own default integration list and never registered `conversationIdIntegration`, so `Sentry.setConversationId()` was a no-op there. The second commit adds it, in the same position Node uses. Part of #24832. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 --- .../langchain/scenario-conversation-id.mjs | 85 +++++++++++++++++ .../suites/tracing/langchain/test.ts | 27 ++++++ .../langchain/v1/scenario-conversation-id.mjs | 62 +++++++++++++ .../suites/tracing/langchain/v1/test.ts | 32 +++++++ .../langgraph/agent-tools-scenario.mjs | 7 +- .../suites/tracing/langgraph/test.ts | 7 ++ .../server-utils/src/ai/langchain/index.ts | 5 + .../server-utils/src/ai/langchain/utils.ts | 36 +++++++ .../server-utils/src/ai/langgraph/index.ts | 20 ++-- .../server-utils/src/ai/langgraph/utils.ts | 3 +- .../src/integrations/langchain.ts | 7 +- .../ai/lib/tracing/langchain-utils.test.ts | 66 +++++++++++++ .../tracing/langgraph-invoke-agent.test.ts | 93 ++++++++++++++++++- 13 files changed, 431 insertions(+), 19 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs new file mode 100644 index 000000000000..8fcb48226773 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/scenario-conversation-id.mjs @@ -0,0 +1,85 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import { RunnableLambda, RunnableSequence } from '@langchain/core/runnables'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_conversation_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'The weather is sunny.' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockAnthropicServer(); + const baseUrl = `http://localhost:${server.address().port}`; + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const model = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + temperature: 0, + maxTokens: 50, + apiKey: 'mock-api-key', + clientOptions: { baseURL: baseUrl }, + }); + + const formatStep = RunnableLambda.from(input => `Tell me about: ${input.topic}`).withConfig({ + runName: 'format_prompt', + }); + + const chain = RunnableSequence.from([formatStep, model]).withConfig({ + runName: 'weather_chain', + }); + + const handler = Sentry.createLangChainCallbackHandler(); + + // 1. `configurable.thread_id` is copied into run metadata by LangChain and inherited by every + // child run, so the chain, the lambda step and the chat call all carry it. + await chain.invoke( + { topic: 'weather' }, + { + callbacks: [handler], + configurable: { thread_id: 'thread_from_config' }, + }, + ); + + // 2. `RunnableWithMessageHistory`-style `sessionId` is picked up too. + await chain.invoke( + { topic: 'weather' }, + { + callbacks: [handler], + configurable: { sessionId: 'session_from_config' }, + }, + ); + + // 3. An id set on the scope outranks the one from the config. + Sentry.setConversationId('conversation_from_scope'); + await chain.invoke({ topic: 'weather' }, { callbacks: [handler], configurable: { thread_id: 'thread_ignored' } }); + Sentry.setConversationId(null); + + // 4. No id anywhere leaves the attribute unset. + await chain.invoke({ topic: 'weather' }, { callbacks: [handler] }); + }); + + await Sentry.flush(2000); + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index 1d74840579bc..290cf38bcba6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -1,5 +1,6 @@ import { afterAll, describe, expect } from 'vitest'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_EMBEDDINGS_INPUT, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, @@ -416,6 +417,32 @@ describe('LangChain integration', () => { }); }); + createEsmAndCjsTests(__dirname, 'scenario-conversation-id.mjs', 'instrument.mjs', (createRunner, test) => { + test('derives gen_ai.conversation.id from the invoke config and lets a scope id win', async () => { + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + // Four invocations, each producing the outer chain span, the lambda step span and the chat span. + expect(container.items).toHaveLength(12); + const idsByName = (name: string): (string | undefined)[] => + container.items + .filter(span => span.name === name) + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value as string | undefined); + + // The id reaches the chain span, the child lambda span and the child chat span alike. + const expected = ['thread_from_config', 'session_from_config', 'conversation_from_scope', undefined]; + expect(idsByName('chain weather_chain')).toEqual(expected); + expect(idsByName('chain format_prompt')).toEqual(expected); + expect(idsByName('chat claude-3-5-sonnet-20241022')).toEqual(expected); + }, + }) + .start() + .completed(); + }); + }); + createEsmAndCjsTests(__dirname, 'scenario-chain.mjs', 'instrument-span-streaming.mjs', (createRunner, test) => { test('leads chain span names with the operation when span streaming is enabled', async () => { await createRunner() diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs new file mode 100644 index 000000000000..6d8f246a9d00 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/scenario-conversation-id.mjs @@ -0,0 +1,62 @@ +import { ChatAnthropic } from '@langchain/anthropic'; +import { RunnableLambda, RunnableSequence } from '@langchain/core/runnables'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockAnthropicServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1/messages', (req, res) => { + res.json({ + id: 'msg_conversation_test', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'The weather is sunny.' }], + model: req.body.model, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockAnthropicServer(); + const baseUrl = `http://localhost:${server.address().port}`; + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const model = new ChatAnthropic({ + model: 'claude-3-5-sonnet-20241022', + temperature: 0, + maxTokens: 50, + apiKey: 'mock-api-key', + clientOptions: { baseURL: baseUrl }, + }); + + const formatStep = RunnableLambda.from(input => `Tell me about: ${input.topic}`); + const chain = RunnableSequence.from([formatStep, model]); + + // `@langchain/core` >= 1.1.40 no longer copies `configurable` into run metadata for our handler, + // so the integration copies the conversation id keys itself. + await chain.invoke({ topic: 'weather' }, { configurable: { thread_id: 'thread_from_config' } }); + await chain.invoke({ topic: 'weather' }, { configurable: { sessionId: 'session_from_config' } }); + + Sentry.setConversationId('conversation_from_scope'); + await chain.invoke({ topic: 'weather' }, { configurable: { thread_id: 'thread_ignored' } }); + Sentry.setConversationId(null); + + await chain.invoke({ topic: 'weather' }); + }); + + await Sentry.flush(2000); + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index c451bfe7bd2d..669ac68afa34 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -1,5 +1,6 @@ import { afterAll, describe, expect } from 'vitest'; import { + GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, @@ -209,6 +210,37 @@ describe('LangChain integration (v1)', () => { }, ); + createEsmAndCjsTests( + __dirname, + 'scenario-conversation-id.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('derives gen_ai.conversation.id from the invoke config and lets a scope id win', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const ids = container.items + .sort((a, b) => a.start_timestamp - b.start_timestamp) + .map(span => span.attributes[GEN_AI_CONVERSATION_ID]?.value); + expect(ids).toEqual(['thread_from_config', 'session_from_config', 'conversation_from_scope', undefined]); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + langchain: '^1.0.0', + '@langchain/core': '^1.0.0', + '@langchain/anthropic': '^1.0.0', + }, + }, + ); + createEsmTests( __dirname, 'scenario-openai-before-langchain.mjs', diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs index f499d9eff5f5..ad73f1133f41 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/agent-tools-scenario.mjs @@ -110,9 +110,10 @@ async function run() { name: 'math_assistant', }); - await agent.invoke({ - messages: [new HumanMessage('Calculate (3 + 5) * 4')], - }); + await agent.invoke( + { messages: [new HumanMessage('Calculate (3 + 5) * 4')] }, + { configurable: { thread_id: 'math_thread' } }, + ); }); await Sentry.flush(2000); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 92b94bf07853..3ec4961fb10f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -344,6 +344,13 @@ describe('LangGraph integration', () => { }), ); expect(spans.filter(span => span.attributes[SENTRY_OP]?.value === 'gen_ai.chat')).toHaveLength(3); + + // `thread_id` reaches the agent span and every chat and tool span under it. + const genAiSpans = spans.filter(span => String(span.attributes[SENTRY_OP]?.value).startsWith('gen_ai.')); + expect(genAiSpans).toHaveLength(6); + for (const span of genAiSpans) { + expect(span.attributes[GEN_AI_CONVERSATION_ID]?.value).toBe('math_thread'); + } }, }) .start() diff --git a/packages/server-utils/src/ai/langchain/index.ts b/packages/server-utils/src/ai/langchain/index.ts index e9d7df2a9d3d..9088e24cc523 100644 --- a/packages/server-utils/src/ai/langchain/index.ts +++ b/packages/server-utils/src/ai/langchain/index.ts @@ -28,6 +28,7 @@ import { extractLlmResponseAttributes, extractToolDefinitions, getAgentNameFromMetadata, + getConversationIdFromMetadata, getInvocationParams, } from './utils'; @@ -111,6 +112,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): : operationName, attributes: { ...getAgentNameFromMetadata(metadata), + ...getConversationIdFromMetadata(metadata), ...attributes, [SENTRY_OP]: GEN_AI_CHAT, }, @@ -161,6 +163,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): : operationName, attributes: { ...getAgentNameFromMetadata(metadata), + ...getConversationIdFromMetadata(metadata), ...attributes, [SENTRY_OP]: GEN_AI_CHAT, }, @@ -221,6 +224,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const chainName = runName || chain.name; const attributes: Record = { + ...getConversationIdFromMetadata(metadata), [SENTRY_ORIGIN]: 'auto.ai.langchain', [GEN_AI_OPERATION_NAME]: 'invoke_agent', }; @@ -301,6 +305,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): const toolName = runName || tool.name || 'unknown_tool'; const attributes: Record = { ...getAgentNameFromMetadata(metadata), + ...getConversationIdFromMetadata(metadata), [SENTRY_ORIGIN]: LANGCHAIN_ORIGIN, [GEN_AI_OPERATION_NAME]: 'execute_tool', [GEN_AI_TOOL_NAME]: toolName, diff --git a/packages/server-utils/src/ai/langchain/utils.ts b/packages/server-utils/src/ai/langchain/utils.ts index fa5a4948d519..88104ee87038 100644 --- a/packages/server-utils/src/ai/langchain/utils.ts +++ b/packages/server-utils/src/ai/langchain/utils.ts @@ -4,6 +4,7 @@ import { stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_AGENT_NAME, + GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, @@ -463,6 +464,41 @@ export function getAgentNameFromMetadata(metadata?: Record): Re return attrs; } +/** + * Metadata keys under which LangChain apps carry a conversation id, in order of precedence. + * `thread_id` is the LangGraph checkpointer key and one of the two keys LangSmith groups threads by, + * `session_id` is the other, and `sessionId` is what `RunnableWithMessageHistory` requires in JS. + */ +const CONVERSATION_ID_METADATA_KEYS = ['thread_id', 'session_id', 'sessionId'] as const; + +function findConversationIdEntry(source?: Record): [string, string | number] | undefined { + for (const key of CONVERSATION_ID_METADATA_KEYS) { + const value = source?.[key]; + if ((typeof value === 'string' && value) || (typeof value === 'number' && Number.isFinite(value))) { + return [key, value]; + } + } + return undefined; +} + +/** + * Derive `gen_ai.conversation.id` from run metadata, which child runs inherit from their parent. + * An id from `Sentry.setConversationId()` still wins, since `conversationIdIntegration` applies it on `spanStart`. + */ +export function getConversationIdFromMetadata(metadata?: Record): Record { + const entry = findConversationIdEntry(metadata); + return entry ? { [GEN_AI_CONVERSATION_ID]: String(entry[1]) } : {}; +} + +/** + * The conversation id entry from `config.configurable`, to spread under the run metadata. + * `@langchain/core` >= 1.1.40 only copies `configurable` into metadata for LangSmith tracers, not for our handler. + */ +export function getConversationIdMetadataFromConfig(config: Record): Record { + const entry = findConversationIdEntry(config.configurable as Record | undefined); + return entry ? { [entry[0]]: entry[1] } : {}; +} + export function extractToolDefinitions(extraParams?: Record): string | undefined { const tools = (extraParams?.invocation_params as Record)?.tools ?? diff --git a/packages/server-utils/src/ai/langgraph/index.ts b/packages/server-utils/src/ai/langgraph/index.ts index 6795c3bd6317..c5b2da14ffc1 100644 --- a/packages/server-utils/src/ai/langgraph/index.ts +++ b/packages/server-utils/src/ai/langgraph/index.ts @@ -2,7 +2,6 @@ import { SPAN_STATUS_ERROR, startSpan, stringify } from '@sentry/core'; import { GEN_AI_AGENT_NAME, - GEN_AI_CONVERSATION_ID, GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, GEN_AI_PIPELINE_NAME, @@ -26,7 +25,11 @@ import { setResponseAttributes, wrapToolsWithSpans, } from './utils'; -import { _INTERNAL_mergeLangChainCallbackHandler } from '../langchain/utils'; +import { + getConversationIdMetadataFromConfig, + _INTERNAL_mergeLangChainCallbackHandler, + getConversationIdFromMetadata, +} from '../langchain/utils'; let _insideCreateReactAgent = false; @@ -96,10 +99,13 @@ export function instrumentCompiledGraphInvoke( return new Proxy(originalInvoke, { apply(target, thisArg, args: unknown[]): Promise { const modelName = llm?.modelName ?? llm?.model; + const config = args.length > 1 ? (args[1] as Record | undefined) : undefined; return startSpan( { name: 'invoke_agent', attributes: { + // Set before `spanStart`, so an id from `Sentry.setConversationId()` wins, as on the child spans + ...getConversationIdFromMetadata(config?.configurable as Record | undefined), [SENTRY_ORIGIN]: LANGGRAPH_ORIGIN, [SENTRY_OP]: GEN_AI_INVOKE_AGENT, [GEN_AI_OPERATION_NAME]: 'invoke_agent', @@ -119,15 +125,6 @@ export function instrumentCompiledGraphInvoke( span.setAttribute(GEN_AI_REQUEST_MODEL, modelName); } - // Extract thread_id from the config (second argument) - // LangGraph uses config.configurable.thread_id for conversation/session linking - const config = args.length > 1 ? (args[1] as Record | undefined) : undefined; - const configurable = config?.configurable as Record | undefined; - const threadId = configurable?.thread_id; - if (threadId && typeof threadId === 'string') { - span.setAttribute(GEN_AI_CONVERSATION_ID, threadId); - } - // Inject callback handler and agent name into invoke config if (sentryCallbackHandler) { const invokeConfig = (args[1] ?? {}) as Record; @@ -135,6 +132,7 @@ export function instrumentCompiledGraphInvoke( const existingMetadata = (invokeConfig.metadata ?? {}) as Record; invokeConfig.metadata = { + ...getConversationIdMetadataFromConfig(invokeConfig), ...existingMetadata, __sentry_langgraph__: true, ...(typeof graphName === 'string' ? { lc_agent_name: graphName } : {}), diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index 94e39318d10e..d8fedd2dbb0e 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -21,7 +21,7 @@ import { import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { BaseChatModel, LangChainMessage } from '../langchain/types'; -import { normalizeLangChainMessages } from '../langchain/utils'; +import { getConversationIdFromMetadata, normalizeLangChainMessages } from '../langchain/utils'; import { LANGGRAPH_ORIGIN } from './constants'; import type { CompiledGraph, LangGraphOptions, LangGraphTool } from './types'; @@ -89,6 +89,7 @@ export function wrapToolsWithSpans(tools: unknown[], options: LangGraphOptions, if (typeof callAgentName === 'string') { spanAttributes[GEN_AI_AGENT_NAME] = callAgentName; } + Object.assign(spanAttributes, getConversationIdFromMetadata(callConfig?.metadata as Record)); if (toolDescription) { spanAttributes[GEN_AI_TOOL_DESCRIPTION] = toolDescription; diff --git a/packages/server-utils/src/integrations/langchain.ts b/packages/server-utils/src/integrations/langchain.ts index d8513a80b926..e623bbbed9f0 100644 --- a/packages/server-utils/src/integrations/langchain.ts +++ b/packages/server-utils/src/integrations/langchain.ts @@ -7,7 +7,7 @@ import { createLangChainCallbackHandler } from '../ai/langchain'; import { LANGCHAIN_INTEGRATION_NAME } from '../ai/langchain/constants'; import { _INTERNAL_getLangChainEmbeddingsSpanOptions } from '../ai/langchain/embeddings'; import type { LangChainOptions } from '../ai/langchain/types'; -import { _INTERNAL_mergeLangChainCallbackHandler } from '../ai/langchain/utils'; +import { getConversationIdMetadataFromConfig, _INTERNAL_mergeLangChainCallbackHandler } from '../ai/langchain/utils'; import { MISTRAL_INTEGRATION_NAME } from '../ai/mistral/constants'; import { OPENAI_INTEGRATION_NAME } from '../ai/openai/constants'; import { GROQ_INTEGRATION_NAME } from './groq'; @@ -91,6 +91,11 @@ function instrumentChatModels(options: LangChainOptions): void { } callOptions.callbacks = _INTERNAL_mergeLangChainCallbackHandler(callOptions.callbacks, sentryHandler); + + const conversationIdMetadata = getConversationIdMetadataFromConfig(callOptions); + if (Object.keys(conversationIdMetadata).length) { + callOptions.metadata = { ...conversationIdMetadata, ...(callOptions.metadata as Record) }; + } }; for (const channelName of [CHANNELS.LANGCHAIN_CHAT_MODEL_INVOKE, CHANNELS.LANGCHAIN_CHAT_MODEL_STREAM]) { diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts index b0c6e3d16eed..a8038425e158 100644 --- a/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langchain-utils.test.ts @@ -1,7 +1,10 @@ +import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; import { describe, expect, it, vi } from 'vitest'; import type { LangChainMessage } from '../../../../src/ai/langchain/types'; import { _INTERNAL_mergeLangChainCallbackHandler, + getConversationIdFromMetadata, + getConversationIdMetadataFromConfig, normalizeLangChainMessages, } from '../../../../src/ai/langchain/utils'; @@ -149,3 +152,66 @@ describe('_INTERNAL_mergeLangChainCallbackHandler', () => { expect(_INTERNAL_mergeLangChainCallbackHandler(existing, sentryHandler)).toBe(existing); }); }); + +describe('getConversationIdFromMetadata', () => { + it('reads thread_id', () => { + expect(getConversationIdFromMetadata({ thread_id: 'thread-1' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'thread-1', + }); + }); + + it('falls back to session_id, then sessionId', () => { + expect(getConversationIdFromMetadata({ session_id: 'session-1' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'session-1', + }); + expect(getConversationIdFromMetadata({ sessionId: 'session-2' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'session-2', + }); + }); + + it('prefers thread_id over the session keys', () => { + expect( + getConversationIdFromMetadata({ sessionId: 'session-1', session_id: 'session-2', thread_id: 'thread-1' }), + ).toEqual({ [GEN_AI_CONVERSATION_ID]: 'thread-1' }); + }); + + it('stringifies a numeric id', () => { + expect(getConversationIdFromMetadata({ thread_id: 42 })).toEqual({ [GEN_AI_CONVERSATION_ID]: '42' }); + }); + + it('ignores empty, non-primitive and missing values', () => { + expect(getConversationIdFromMetadata({ thread_id: '' })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: { id: 'x' } })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: null })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: NaN })).toEqual({}); + expect(getConversationIdFromMetadata({ thread_id: Infinity })).toEqual({}); + expect(getConversationIdFromMetadata({})).toEqual({}); + expect(getConversationIdFromMetadata(undefined)).toEqual({}); + }); + + it('skips an empty thread_id and uses the next key', () => { + expect(getConversationIdFromMetadata({ thread_id: '', sessionId: 'session-1' })).toEqual({ + [GEN_AI_CONVERSATION_ID]: 'session-1', + }); + }); +}); + +describe('getConversationIdMetadataFromConfig', () => { + it('picks the first conversation id key out of configurable', () => { + expect( + getConversationIdMetadataFromConfig({ + configurable: { thread_id: 'thread-1', sessionId: 42, model: 'gpt-4o' }, + }), + ).toEqual({ thread_id: 'thread-1' }); + }); + + it('skips values that are not a valid id', () => { + expect( + getConversationIdMetadataFromConfig({ configurable: { thread_id: '', session_id: { id: 'x' }, sessionId: 42 } }), + ).toEqual({ sessionId: 42 }); + }); + + it('returns nothing without configurable', () => { + expect(getConversationIdMetadataFromConfig({})).toEqual({}); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts b/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts index 1e8a204bd85e..3f894efae3ea 100644 --- a/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts +++ b/packages/server-utils/test/ai/lib/tracing/langgraph-invoke-agent.test.ts @@ -1,8 +1,17 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core'; +import { + conversationIdIntegration, + getCurrentScope, + getMainCarrier, + setCurrentClient, + spanToStaticSpanJSON, + startSpan, +} from '@sentry/core'; import type { Span } from '@sentry/core'; +import { GEN_AI_CONVERSATION_ID } from '@sentry/conventions/attributes'; import { instrumentCompiledGraphInvoke } from '../../../../src/ai/langgraph'; import type { CompiledGraph } from '../../../../src/ai/langgraph/types'; +import { wrapToolsWithSpans } from '../../../../src/ai/langgraph/utils'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; describe('LangGraph invoke_agent span names', () => { @@ -20,6 +29,7 @@ describe('LangGraph invoke_agent span names', () => { dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, traceLifecycle, + integrations: [conversationIdIntegration()], }), ); setCurrentClient(client); @@ -30,14 +40,14 @@ describe('LangGraph invoke_agent span names', () => { return endedSpans; } - async function invokeGraph(compileOptions: Record): Promise { + async function invokeGraph(compileOptions: Record, config?: Record): Promise { const invoke = instrumentCompiledGraphInvoke( async () => ({ messages: [] }), {} as CompiledGraph, compileOptions, {}, ); - await invoke({}); + await invoke({}, config); } it('names the span `{operation} {agent}` when an agent name is present', async () => { @@ -54,3 +64,80 @@ describe('LangGraph invoke_agent span names', () => { expect(spanToStaticSpanJSON(endedSpans[0]!).description).toBe('invoke_agent'); }); }); + +describe('LangGraph conversation id', () => { + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + afterEach(() => { + getMainCarrier().__SENTRY__ = undefined; + }); + + function setupClient(): Span[] { + const client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, + integrations: [conversationIdIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const endedSpans: Span[] = []; + client.on('spanEnd', span => endedSpans.push(span)); + return endedSpans; + } + + async function invokeGraph(config: Record): Promise { + const endedSpans = setupClient(); + const invoke = instrumentCompiledGraphInvoke(async () => ({ messages: [] }), {} as CompiledGraph, {}, {}); + await invoke({}, config); + return endedSpans[0]!; + } + + it('reads the same metadata keys as the LangChain spans', async () => { + const span = await invokeGraph({ configurable: { sessionId: 'session-1' } }); + + expect(spanToStaticSpanJSON(span).data[GEN_AI_CONVERSATION_ID]).toBe('session-1'); + }); + + it('lets an id set on the scope win over configurable.thread_id', async () => { + getCurrentScope().setConversationId('scope-id'); + const span = await invokeGraph({ configurable: { thread_id: 'thread-1' } }); + + expect(spanToStaticSpanJSON(span).data[GEN_AI_CONVERSATION_ID]).toBe('scope-id'); + }); + + it('copies the conversation id from configurable into the metadata the graph runs with', async () => { + setupClient(); + let receivedConfig: Record | undefined; + const invoke = instrumentCompiledGraphInvoke( + async (_input: unknown, config: unknown) => { + receivedConfig = config as Record; + return { messages: [] }; + }, + {} as CompiledGraph, + {}, + {}, + undefined, + {}, + ); + + await invoke({}, { configurable: { sessionId: 'session-1' }, metadata: { user: 'u' } }); + + expect(receivedConfig?.metadata).toEqual({ sessionId: 'session-1', user: 'u', __sentry_langgraph__: true }); + }); + + it('sets the conversation id on wrapped tool spans from the call metadata', async () => { + const endedSpans = setupClient(); + const tool = { name: 'add', invoke: async () => '3' }; + wrapToolsWithSpans([tool], {}); + + await startSpan({ name: 'root' }, () => tool.invoke({}, { metadata: { thread_id: 'thread-1' } })); + + const toolSpan = endedSpans.find(span => spanToStaticSpanJSON(span).description === 'execute_tool add')!; + expect(spanToStaticSpanJSON(toolSpan).data[GEN_AI_CONVERSATION_ID]).toBe('thread-1'); + }); +}); From 5fc18dc416aeed33fc56227fd3714b7c58f21d97 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 7 Oct 2026 09:08:57 +0200 Subject: [PATCH 66/73] fix(remix): Pass the project when creating a release (#25079) The release API rejects a release without a project, and the project given to the `sentry` SDK does not reach `release.create`. Since the move to that SDK (#23398), `sentry-upload-sourcemaps` failed with `400 Bad Request` on every run. The unit test mocked the SDK, so it did not catch it. Reproduced against a real org. Both upload scripts take the project from `--project`, then `SENTRY_PROJECT`, and fail with a clear message when neither is set. `@sentry/bundler-plugins` already did this. The React Router Vite plugin has the same bug, fixed separately. Fixes #24989 --------- Co-authored-by: Claude Fable 5.1 --- packages/remix/scripts/createRelease.js | 9 ++++- .../sentry-remix-v3-upload-sourcemaps.mjs | 9 ++++- .../test/scripts/upload-sourcemaps.test.ts | 37 +++++++++++++++---- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/remix/scripts/createRelease.js b/packages/remix/scripts/createRelease.js index c6fe5cb9ee2f..e55e865b8fa3 100644 --- a/packages/remix/scripts/createRelease.js +++ b/packages/remix/scripts/createRelease.js @@ -25,7 +25,14 @@ async function createRelease(argv, URL_PREFIX, BUILD_PATH) { release = argv.release; } - await sentry.release.create({ orgVersion: release }); + // The release API requires a project, and the one given to the SDK does not reach this call. + const project = argv.project ?? process.env.SENTRY_PROJECT; + if (!project) { + throw new Error( + '[sentry] A project is required to create a release. Pass `--project` or set `SENTRY_PROJECT` as environment variable.', + ); + } + await sentry.release.create({ orgVersion: release, project }); try { await sentry.sourcemap.upload({ diff --git a/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs b/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs index 8cc963fd57ea..9f9df060fb30 100755 --- a/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs +++ b/packages/remix/scripts/sentry-remix-v3-upload-sourcemaps.mjs @@ -21,6 +21,11 @@ let keepOutput = false; try { const values = parseOptions(); keepOutput = Boolean(values['keep-output'] || values['out-dir']); + const org = values.org ?? process.env.SENTRY_ORG; + const project = values.project ?? process.env.SENTRY_PROJECT; + if (!values['dry-run'] && !project) { + fail('a project is required for the upload. Pass `--project` or set `SENTRY_PROJECT` as environment variable.'); + } const { emitAssets } = await import('@sentry/remix/v3'); @@ -63,11 +68,11 @@ try { console.log('[sentry] --dry-run, skipping upload'); } else { const { createSentrySDK } = await import('sentry'); - const sentry = createSentrySDK({ url: values.url, org: values.org, project: values.project }); + const sentry = createSentrySDK({ url: values.url, org, project }); const release = values.release ?? (await sentry.release['propose-version']()).version; try { // The project has to be named here; the SDK default does not reach release creation. - await sentry.release.create({ orgVersion: release, project: values.project }); + await sentry.release.create({ orgVersion: release, project }); // Modules keep their served names (`.ts`, `.tsx`), which the upload skips by default. Only module // extensions go here: the upload pairs each module with the `.map` sibling `emitAssets` wrote. const ext = [...new Set(emitted.map(asset => path.extname(asset.file)))].join(','); diff --git a/packages/remix/test/scripts/upload-sourcemaps.test.ts b/packages/remix/test/scripts/upload-sourcemaps.test.ts index cce1ed03f269..426199331c8c 100644 --- a/packages/remix/test/scripts/upload-sourcemaps.test.ts +++ b/packages/remix/test/scripts/upload-sourcemaps.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const createMock = vi.fn(); const uploadSourceMapsMock = vi.fn(); @@ -48,12 +48,16 @@ beforeEach(() => { proposeVersionMock.mockClear(); }); +afterEach(() => { + vi.unstubAllEnvs(); +}); + describe('createRelease', () => { it('should use release param when given', async () => { - await createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build'); + await createRelease({ release: '0.1.2.3', project: 'my-project' }, '~/build/', 'public/build'); expect(proposeVersionMock).not.toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3' }); + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3', project: 'my-project' }); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', release: '0.1.2.3', @@ -64,10 +68,10 @@ describe('createRelease', () => { }); it('should call `proposeVersion` when release param is not given.', async () => { - await createRelease({}, '~/build/', 'public/build'); + await createRelease({ project: 'my-project' }, '~/build/', 'public/build'); expect(proposeVersionMock).toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4', project: 'my-project' }); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', release: '0.1.2.3.4', @@ -80,6 +84,7 @@ describe('createRelease', () => { it('should use given buildPath and urlPrefix over the defaults when given.', async () => { await createRelease( { + project: 'my-project', urlPrefix: '~/build/', buildPath: 'public/build', }, @@ -88,7 +93,7 @@ describe('createRelease', () => { ); expect(proposeVersionMock).toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4', project: 'my-project' }); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', release: '0.1.2.3.4', @@ -101,7 +106,7 @@ describe('createRelease', () => { it('logs an error when uploadSourceMaps fails', async () => { uploadSourceMapsMock.mockRejectedValue(new Error('Failed to upload sourcemaps')); - await createRelease({}, '~/build/', 'public/build'); + await createRelease({ project: 'my-project' }, '~/build/', 'public/build'); expect(uploadSourceMapsMock).toHaveBeenCalledWith({ directory: 'public/build', @@ -118,10 +123,26 @@ describe('createRelease', () => { it('logs an error when finalize fails', async () => { finalizeMock.mockRejectedValue(new Error('Failed to finalize release')); - await createRelease({}, '~/build/', 'public/build'); + await createRelease({ project: 'my-project' }, '~/build/', 'public/build'); expect(consoleWarnSpy).toHaveBeenCalledWith('[sentry] Failed to finalize release.'); }); + + it('falls back to SENTRY_PROJECT for the release project', async () => { + vi.stubEnv('SENTRY_PROJECT', 'env-project'); + + await createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build'); + + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3', project: 'env-project' }); + }); + + it('fails before creating a release when no project is known', async () => { + vi.stubEnv('SENTRY_PROJECT', ''); + + await expect(createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build')).rejects.toThrow('--project'); + + expect(createMock).not.toHaveBeenCalled(); + }); }); // To avoid `--isolatedModules` flag as we're not importing From 2a085beeacd094b74e5bc78024e6c6591bfe50d1 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 7 Oct 2026 09:09:27 +0200 Subject: [PATCH 67/73] fix(react-router): Pass the project when creating a release (#25089) The release API rejects a release without a project, and the project given to the `sentry` SDK does not reach `release.create`. Since the move to that SDK (#23398), the Vite plugin logged `[Sentry] Could not create release` on every build; the upload itself still ran, so builds succeeded without a release. The unit test mocked the SDK, so it did not catch it. The plugin now passes the project it already has, as `@sentry/bundler-plugins` does. Same bug in the Remix upload scripts, fixed in #25079. Refs #24989 Co-authored-by: Claude Fable 5.1 --- .../react-router/src/vite/buildEnd/handleOnBuildEnd.ts | 3 ++- .../test/vite/buildEnd/handleOnBuildEnd.test.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts index 23744ed79722..3af80b186e14 100644 --- a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts +++ b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts @@ -67,7 +67,8 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo // check if release should be created if (release?.name) { try { - await sentry.release.create({ orgVersion: release.name }); + // The release API requires a project, and the one given to the SDK does not reach this call. + await sentry.release.create({ orgVersion: release.name, project }); } catch (error) { // eslint-disable-next-line no-console console.error('[Sentry] Could not create release', error); diff --git a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts index 7f907d4e8aca..9bb18672ba3c 100644 --- a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts +++ b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts @@ -85,7 +85,10 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: 'v1.0.0' }); + expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ + orgVersion: 'v1.0.0', + project: 'test-project', + }); }); it('resolves root-level BuildTimeOptionsBase options for release creation and source map upload', async () => { @@ -115,7 +118,7 @@ describe('sentryOnBuildEnd', () => { url: undefined, headers: undefined, }); - expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: '1.2.3' }); + expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: '1.2.3', project: 'my-project' }); expect(mockSentrySdkInstance.sourcemap.upload).toHaveBeenCalledWith({ directory: '/build', release: '1.2.3', From c7ed7caea071d97eeeea5a834f3ab7dd6d4e2b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Wed, 7 Oct 2026 10:30:22 +0300 Subject: [PATCH 68/73] test(cloudflare): Add Workers AI tracing tests for Clef (#25027) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloudflare documents Clef (`@cf/cloudflare/clef`) as a drop-in for Jev with the same `state` + `questions` request shape, so it already goes through the `evaluate` path from #24833. Only Jev had tests, so this adds a Clef integration test and a real-Worker E2E test. The integration mock returns the documented Clef response, which has no `{ state, result }` wrapper (the Jev binding response has one). That makes it cover the unwrapped branch of the evaluate response handling, which the Jev test does not reach. The E2E test calls the real model, so it is the one that confirms what the binding actually returns. The docs do not give a `model` value for the response, so it only asserts that it contains `clef`. Fixes #25011 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../suites/tracing/workers-ai/index.ts | 9 +++ .../suites/tracing/workers-ai/mocks.ts | 9 +++ .../suites/tracing/workers-ai/test.ts | 67 ++++++++++++++++--- .../src/index.ts | 8 +++ .../tests/send-to-sentry.test.ts | 21 ++++++ .../server-utils/src/ai/workers-ai/utils.ts | 7 +- .../ai/lib/utils/workers-ai-utils.test.ts | 4 ++ 7 files changed, 115 insertions(+), 10 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts index 42d2d895fea0..5f793c2c2e54 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/index.ts @@ -42,6 +42,15 @@ export default Sentry.withSentry( return new Response(JSON.stringify(result)); } + if (url.pathname === '/evaluate-clef') { + const result = await ai.run('@cf/cloudflare/clef', { + model: 'clef', + state: 'Checkout has been failing for every customer for the last hour.', + questions: { urgent: { type: 'noul', instructions: 'Is this support request urgent?' } }, + }); + return new Response(JSON.stringify(result)); + } + if (url.pathname === '/stream') { const stream = (await ai.run('@cf/meta/llama-3.1-8b-instruct', { messages: [{ role: 'user', content: 'What is the capital of France?' }], diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts index e0a26d02bf86..0d7e599cca52 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/mocks.ts @@ -34,6 +34,15 @@ export class MockAi { }; } + // Unwrapped, as in the documented Clef output schema, so this covers the path without `{ state, result }`. + if (model === '@cf/cloudflare/clef') { + return { + model: 'clef', + answers: { urgent: { type: 'noul', noul: 0.98 } }, + usage: { input_tokens: 412, output_tokens: 1 }, + }; + } + if (inputs?.stream === true) { return createSseStream([ '{"response":"The capital "}', diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts index 6ed31514dfe4..600062d6eb0a 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/workers-ai/test.ts @@ -17,6 +17,9 @@ import { SENTRY_SEGMENT_NAME, SENTRY_TRACE_LIFECYCLE, SENTRY_ENVIRONMENT, + SENTRY_IS_LOCALHOST, + SENTRY_OP, + SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { SDK_VERSION } from '@sentry/core'; import { expect, it } from 'vitest'; @@ -45,8 +48,8 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => { status: 'ok', is_segment: false, attributes: { - 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, - 'sentry.op': { value: 'gen_ai.chat', type: 'string' }, + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.chat', type: 'string' }, [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, [GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' }, [GEN_AI_REQUEST_MODEL]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' }, @@ -64,7 +67,7 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => { type: 'string', value: 'The capital of France is Paris.', }, - 'sentry.is_localhost': { value: true, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, @@ -99,8 +102,8 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) = status: 'ok', is_segment: false, attributes: { - 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, - 'sentry.op': { value: 'gen_ai.chat', type: 'string' }, + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.chat', type: 'string' }, [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, [GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' }, [GEN_AI_REQUEST_MODEL]: { value: '@cf/meta/llama-3.1-8b-instruct', type: 'string' }, @@ -118,7 +121,7 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) = type: 'string', value: 'The capital of France is Paris.', }, - 'sentry.is_localhost': { value: true, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, @@ -150,8 +153,8 @@ it('traces a TypeSafe Jev evaluation like the TypeSafe integration', async ({ si status: 'ok', is_segment: false, attributes: { - 'sentry.origin': { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, - 'sentry.op': { value: 'gen_ai.evaluate', type: 'string' }, + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.evaluate', type: 'string' }, [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, [GEN_AI_OPERATION_NAME]: { value: 'evaluate', type: 'string' }, [GEN_AI_REQUEST_MODEL]: { value: 'typesafe/jev', type: 'string' }, @@ -164,7 +167,7 @@ it('traces a TypeSafe Jev evaluation like the TypeSafe integration', async ({ si type: 'string', value: '[{"type":"evaluation","answers":{"is_urgent":{"type":"noul","noul":0.97}}}]', }, - 'sentry.is_localhost': { value: true, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, @@ -180,6 +183,52 @@ it('traces a TypeSafe Jev evaluation like the TypeSafe integration', async ({ si await runner.completed(); }); +it('traces a Clef evaluation like a TypeSafe Jev evaluation', async ({ signal }) => { + const runner = createRunner(__dirname) + .ignore('event') + .expect(envelope => { + const spans = getSpansFromEnvelope(envelope); + const segmentSpan = spans.find(span => span.is_segment); + + const genAiSpans = spans.filter(span => getSpanOp(span)?.startsWith('gen_ai.')); + expect(genAiSpans).toHaveLength(1); + + expect(genAiSpans[0]).toEqual( + expect.objectContaining({ + name: 'evaluate @cf/cloudflare/clef', + status: 'ok', + is_segment: false, + attributes: { + [SENTRY_ORIGIN]: { value: 'auto.ai.cloudflare.workers_ai', type: 'string' }, + [SENTRY_OP]: { value: 'gen_ai.evaluate', type: 'string' }, + [GEN_AI_PROVIDER_NAME]: { value: 'cloudflare.workers_ai', type: 'string' }, + [GEN_AI_OPERATION_NAME]: { value: 'evaluate', type: 'string' }, + [GEN_AI_REQUEST_MODEL]: { value: '@cf/cloudflare/clef', type: 'string' }, + [GEN_AI_RESPONSE_MODEL]: { value: 'clef', type: 'string' }, + [GEN_AI_USAGE_INPUT_TOKENS]: { value: 412, type: 'integer' }, + [GEN_AI_USAGE_OUTPUT_TOKENS]: { value: 1, type: 'integer' }, + [GEN_AI_USAGE_TOTAL_TOKENS]: { value: 413, type: 'integer' }, + // collect only output messages + [GEN_AI_OUTPUT_MESSAGES]: { + type: 'string', + value: '[{"type":"evaluation","answers":{"urgent":{"type":"noul","noul":0.98}}}]', + }, + [SENTRY_IS_LOCALHOST]: { value: true, type: 'boolean' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: segmentSpan!.name, type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: segmentSpan!.span_id, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.cloudflare', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, + }, + }), + ); + }) + .start(signal); + await runner.makeRequest('get', '/evaluate-clef'); + await runner.completed(); +}); + // The Workers AI integration deliberately does not call `captureException` itself. // When a `run` call fails, the error must bubble up out of the fetch handler and be // reported by the top-level Cloudflare instrumentation instead — so it shows up in diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts index 87ae6c9cff41..990859d99f7e 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/src/index.ts @@ -38,6 +38,14 @@ export default { }); return Response.json({ traceId: spanContext?.traceId }); } + case '/test-workers-ai-clef': { + await env.AI.run('@cf/cloudflare/clef', { + model: 'clef', + state: 'Checkout has been failing for every customer for the last hour.', + questions: { urgent: { type: 'noul', instructions: 'Is this support request urgent?' } }, + }); + return Response.json({ traceId: spanContext?.traceId }); + } case '/test-span': return Response.json({ spanId: spanContext?.spanId, traceId: spanContext?.traceId }); case '/test-workflow-sleep': { diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts index b391f2f8244b..f5ce99ab2a0f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-send-to-sentry/tests/send-to-sentry.test.ts @@ -114,3 +114,24 @@ test('Sends a Workers AI gen_ai.evaluate span for a TypeSafe Jev call to Sentry' 'gen_ai.output.messages': expect.stringContaining('"is_urgent"'), }); }); + +test('Sends a Workers AI gen_ai.evaluate span for a Clef call to Sentry', async () => { + const { traceId }: { traceId: string } = JSON.parse(await fetchFromWorker(`${workerUrl}/test-workers-ai-clef`, 200)); + + console.log(`Polling for gen_ai.evaluate span: sentry trace view ${traceTarget(traceId)}`); + + let spanId: string | undefined; + await expect + .poll(() => (spanId = findSpanInTrace(traceId, 'gen_ai.evaluate')?.event_id), EVENT_POLLING_OPTIONS) + .toBeDefined(); + + await expect + .poll(() => fetchSpanAttributes(traceId, spanId!), EVENT_POLLING_OPTIONS) + .toMatchObject({ + 'gen_ai.operation.name': 'evaluate', + 'gen_ai.request.model': '@cf/cloudflare/clef', + 'gen_ai.response.model': expect.stringContaining('clef'), + 'gen_ai.input.messages': expect.stringContaining('Checkout has been failing'), + 'gen_ai.output.messages': expect.stringContaining('"urgent"'), + }); +}); diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index 2ccb9d1125c7..e2ef7be9c438 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -35,12 +35,17 @@ export const WORKERS_AI_OPERATION_SPAN_OPS: Record model.startsWith(prefix))) { return 'evaluate'; } if (inputs && typeof inputs === 'object') { diff --git a/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts b/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts index 9976ff52706a..40abfadf8f2f 100644 --- a/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts +++ b/packages/server-utils/test/ai/lib/utils/workers-ai-utils.test.ts @@ -65,6 +65,10 @@ describe('workers-ai utils', () => { expect(getOperationName('typesafe/jev-1.13', { state: 'Help!', questions: {} })).toBe('evaluate'); }); + it('returns "evaluate" for Cloudflare Clef models', () => { + expect(getOperationName('@cf/cloudflare/clef', { state: 'Help!', questions: {} })).toBe('evaluate'); + }); + it('does not return "evaluate" for other TypeSafe models', () => { expect(getOperationName('typesafe/other', { prompt: 'Hello' })).toBe('chat'); }); From 75ae1647c1bdad2ff9a588b901903c5df653c661 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 7 Oct 2026 10:55:28 +0200 Subject: [PATCH 69/73] fix(nextjs): Don't capture forbidden() and unauthorized() as errors (#25088) Next.js 16.4 canary drops the `authInterrupts` flag, so `forbidden()`/`unauthorized()` throws (`NEXT_HTTP_ERROR_FALLBACK;403`/`;401`) now need to be treated as control flow like `notFound()`. Co-authored-by: Claude Opus 5.5 --- .../app/auth-interrupts/forbidden/page.tsx | 7 ++ .../route-handler/forbidden/route.ts | 7 ++ .../route-handler/unauthorized/route.ts | 7 ++ .../auth-interrupts/server-action/page.tsx | 34 ++++++++ .../app/auth-interrupts/unauthorized/page.tsx | 7 ++ .../nextjs-16/next.config.ts | 1 + .../nextjs-16/tests/auth-interrupts.test.ts | 85 +++++++++++++++++++ .../src/common/nextNavigationErrorUtils.ts | 14 +++ .../common/withServerActionInstrumentation.ts | 5 ++ .../wrapGenerationFunctionWithSentry.ts | 8 ++ .../src/common/wrapRouteHandlerWithSentry.ts | 9 ++ .../common/wrapServerComponentWithSentry.ts | 8 ++ .../common/nextNavigationErrorUtils.test.ts | 17 ++++ .../wrapServerComponentWithSentry.test.ts | 14 +++ 14 files changed, 223 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx new file mode 100644 index 000000000000..d59c333380fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/forbidden/page.tsx @@ -0,0 +1,7 @@ +import { forbidden } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function ForbiddenPage() { + forbidden(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts new file mode 100644 index 000000000000..349e85337344 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/forbidden/route.ts @@ -0,0 +1,7 @@ +import { forbidden } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + forbidden(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts new file mode 100644 index 000000000000..4b56478b45b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/route-handler/unauthorized/route.ts @@ -0,0 +1,7 @@ +import { unauthorized } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + unauthorized(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx new file mode 100644 index 000000000000..326d27592ccc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/server-action/page.tsx @@ -0,0 +1,34 @@ +import * as Sentry from '@sentry/nextjs'; +import { headers } from 'next/headers'; +import { forbidden, unauthorized } from 'next/navigation'; + +export default function AuthInterruptsServerActionPage() { + async function forbiddenServerAction() { + 'use server'; + return await Sentry.withServerActionInstrumentation('forbiddenServerAction', { headers: await headers() }, () => { + forbidden(); + }); + } + + async function unauthorizedServerAction() { + 'use server'; + return await Sentry.withServerActionInstrumentation( + 'unauthorizedServerAction', + { headers: await headers() }, + () => { + unauthorized(); + }, + ); + } + + return ( + <> +
+ +
+
+ +
+ + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx new file mode 100644 index 000000000000..8929e86d1613 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/auth-interrupts/unauthorized/page.tsx @@ -0,0 +1,7 @@ +import { unauthorized } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function UnauthorizedPage() { + unauthorized(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts index 8f6c5edfbcca..9278da016887 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/next.config.ts @@ -6,6 +6,7 @@ process.env.VERCEL = '1'; const nextConfig: NextConfig = { experimental: { + authInterrupts: true, sri: { algorithm: 'sha256', }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts new file mode 100644 index 000000000000..a24f6e2c7108 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/auth-interrupts.test.ts @@ -0,0 +1,85 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpansUntilSegment, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +const cases = [ + { kind: 'forbidden', label: 'Forbidden', status: 403, statusMessage: 'permission_denied' }, + { kind: 'unauthorized', label: 'Unauthorized', status: 401, statusMessage: 'unauthenticated' }, +] as const; + +function waitForAuthInterruptError(status: number): Promise { + return waitForError('nextjs-16', errorEvent => { + return JSON.stringify(errorEvent.exception ?? {}).includes(`NEXT_HTTP_ERROR_FALLBACK;${status}`); + }); +} + +// The error would be sent in the same flush as the spans, so a short grace period is enough. +async function expectNoError(errorPromise: Promise): Promise { + const result = await Promise.race([ + errorPromise.then(() => 'error'), + new Promise(resolve => setTimeout(() => resolve('timeout'), 2000)), + ]); + expect(result).toBe('timeout'); +} + +for (const { kind, label, status, statusMessage } of cases) { + test(`Does not capture ${kind}() in a server component and marks the render span`, async ({ page }) => { + const errorPromise = waitForAuthInterruptError(status); + const spansPromise = collectStreamedSpansUntilSegment('nextjs-16', `GET /auth-interrupts/${kind}`); + + const response = await page.goto(`/auth-interrupts/${kind}`); + expect(response?.status()).toBe(status); + + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; + expect(segmentSpan.status).toBe('error'); + expect(segmentSpan.attributes['http.response.status_code']?.value).toBe(status); + + // Server components are only wrapped by our webpack loader, so only webpack builds set a status on the render span. + if (!segmentSpan.attributes['turbopack']) { + expect(spans).toContainEqual( + expect.objectContaining({ + name: `render route (app) /auth-interrupts/${kind}`, + status: 'error', + attributes: expect.objectContaining({ + 'sentry.status.message': { value: statusMessage, type: 'string' }, + }), + }), + ); + } + + await expectNoError(errorPromise); + }); + + test(`Does not capture ${kind}() in a server action and marks the action span`, async ({ page }) => { + const errorPromise = waitForAuthInterruptError(status); + const spanPromise = waitForStreamedSpan('nextjs-16', span => { + return span.name === `${kind}ServerAction` && span.is_segment; + }); + + await page.goto('/auth-interrupts/server-action'); + await page.getByText(`Run ${label} Action`).click(); + + const span = await spanPromise; + expect(span.status).toBe('error'); + expect(span.attributes['sentry.status.message']?.value).toBe(statusMessage); + + await expectNoError(errorPromise); + }); + + test(`Does not capture ${kind}() in a route handler and sets the response status`, async ({ request }) => { + const errorPromise = waitForAuthInterruptError(status); + const spanPromise = waitForStreamedSpan('nextjs-16', span => { + return span.name === `GET /auth-interrupts/route-handler/${kind}` && span.is_segment; + }); + + const response = await request.get(`/auth-interrupts/route-handler/${kind}`); + expect(response.status()).toBe(status); + + const span = await spanPromise; + expect(span.status).toBe('error'); + expect(span.attributes['sentry.status.message']?.value).toBe(statusMessage); + expect(span.attributes['http.response.status_code']?.value).toBe(status); + + await expectNoError(errorPromise); + }); +} diff --git a/packages/nextjs/src/common/nextNavigationErrorUtils.ts b/packages/nextjs/src/common/nextNavigationErrorUtils.ts index 94960cc32ca9..9ad2674729ea 100644 --- a/packages/nextjs/src/common/nextNavigationErrorUtils.ts +++ b/packages/nextjs/src/common/nextNavigationErrorUtils.ts @@ -29,6 +29,20 @@ export function isNotFoundNavigationError(subject: unknown): boolean { return hasDigest(subject, digest => ['NEXT_NOT_FOUND', 'NEXT_HTTP_ERROR_FALLBACK;404'].includes(digest)); } +/** + * Returns the HTTP status of a Next.js `forbidden()` (403) or `unauthorized()` (401) error, if input is one. + * https://nextjs.org/docs/app/api-reference/functions/forbidden + */ +export function getAuthInterruptStatusCode(subject: unknown): 401 | 403 | undefined { + if (hasDigest(subject, digest => digest === 'NEXT_HTTP_ERROR_FALLBACK;403')) { + return 403; + } + if (hasDigest(subject, digest => digest === 'NEXT_HTTP_ERROR_FALLBACK;401')) { + return 401; + } + return undefined; +} + /** * Determines whether input is a Next.js redirect error. * https://beta.nextjs.org/docs/api-reference/redirect#redirect diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index e660682282d6..3f4587f80a35 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -6,6 +6,7 @@ import { getActiveSpan, getClient, getIsolationScope, + getSpanStatusFromHttpCode, handleCallbackErrors, hasSpanStreamingEnabled, SPAN_STATUS_ERROR, @@ -16,6 +17,7 @@ import { import { flushSafelyWithTimeout, waitUntil } from '../common/utils/responseEnd'; import { DEBUG_BUILD } from './debug-build'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -144,9 +146,12 @@ async function withServerActionInstrumentationImplementation { // oxlint-disable-next-line typescript/await-thenable -- callback may be async at runtime const result = await handleCallbackErrors(callback, error => { + const authInterruptStatusCode = getAuthInterruptStatusCode(error); if (isNotFoundNavigationError(error)) { // We don't want to report "not-found"s span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (authInterruptStatusCode) { + span.setStatus(getSpanStatusFromHttpCode(authInterruptStatusCode)); } else if (isRedirectNavigationError(error)) { // Redirects are normal Next.js control flow, not errors. Mark the span as OK and end it // early so the surrounding `startSpan` error handler doesn't override the status to diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index d90678ded6ed..29d609dae31d 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -3,6 +3,7 @@ import { captureException, getActiveSpan, getIsolationScope, + getSpanStatusFromHttpCode, handleCallbackErrors, SPAN_STATUS_ERROR, SPAN_STATUS_OK, @@ -10,6 +11,7 @@ import { } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -59,6 +61,12 @@ export function wrapGenerationFunctionWithSentry a return; } + const authInterruptStatusCode = getAuthInterruptStatusCode(error); + if (authInterruptStatusCode) { + span?.setStatus(getSpanStatusFromHttpCode(authInterruptStatusCode)); + return; + } + if (isRedirectNavigationError(error)) { // We don't want to report redirects span?.setStatus({ code: SPAN_STATUS_OK }); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index cbefbc2f079c..7ed7b15eb0ba 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -16,6 +16,7 @@ import { withScope, } from '@sentry/core'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -83,6 +84,7 @@ export function wrapRouteHandlerWithSentry any>( const response: Response = await handleCallbackErrors( () => originalFunction.apply(thisArg, args), error => { + const authInterruptStatusCode = getAuthInterruptStatusCode(error); // Next.js throws errors when calling `redirect()`. We don't wanna report these. if (isRedirectNavigationError(error)) { // Don't do anything @@ -93,6 +95,13 @@ export function wrapRouteHandlerWithSentry any>( if (rootSpan) { setHttpStatus(rootSpan, 404); } + } else if (authInterruptStatusCode) { + if (activeSpan) { + setHttpStatus(activeSpan, authInterruptStatusCode); + } + if (rootSpan) { + setHttpStatus(rootSpan, authInterruptStatusCode); + } } else if (isPrerenderControlFlowError(error)) { // Next.js aborts prerenders by rejecting the promises it handed out. React discards those // rejections, so they are expected, do not affect the response, and must not be reported. diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 6deaef200f67..9f96f5a912c1 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -3,12 +3,14 @@ import { captureException, getActiveSpan, getIsolationScope, + getSpanStatusFromHttpCode, handleCallbackErrors, SPAN_STATUS_ERROR, SPAN_STATUS_OK, winterCGHeadersToDict, } from '@sentry/core'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -54,6 +56,12 @@ export function wrapServerComponentWithSentry any> return; } + const authInterruptStatusCode = getAuthInterruptStatusCode(error); + if (authInterruptStatusCode) { + span?.setStatus(getSpanStatusFromHttpCode(authInterruptStatusCode)); + return; + } + if (isRedirectNavigationError(error)) { // We don't want to report redirects span?.setStatus({ code: SPAN_STATUS_OK }); diff --git a/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts b/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts index 072893b8f53b..e0e2c482b737 100644 --- a/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts +++ b/packages/nextjs/test/common/nextNavigationErrorUtils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + getAuthInterruptStatusCode, isNotFoundNavigationError, isPrerenderControlFlowError, isRedirectNavigationError, @@ -21,6 +22,22 @@ describe('isNotFoundNavigationError', () => { }); }); +describe('getAuthInterruptStatusCode', () => { + it.each([ + ['NEXT_HTTP_ERROR_FALLBACK;403', 403], + ['NEXT_HTTP_ERROR_FALLBACK;401', 401], + ])('returns the status for the %s digest', (digest, status) => { + expect(getAuthInterruptStatusCode(errorWithDigest(digest))).toBe(status); + expect(getAuthInterruptStatusCode(new Error('wrapped', { cause: errorWithDigest(digest) }))).toBe(status); + }); + + it('returns undefined for other errors', () => { + expect(getAuthInterruptStatusCode(errorWithDigest('NEXT_HTTP_ERROR_FALLBACK;404'))).toBeUndefined(); + expect(getAuthInterruptStatusCode(new Error('boom'))).toBeUndefined(); + expect(getAuthInterruptStatusCode({ digest: 'NEXT_HTTP_ERROR_FALLBACK;403' })).toBeUndefined(); + }); +}); + describe('isRedirectNavigationError', () => { it('detects a redirect digest', () => { expect(isRedirectNavigationError(errorWithDigest('NEXT_REDIRECT;/some-path'))).toBe(true); diff --git a/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts b/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts index aed2553c7baa..8556f5bda8df 100644 --- a/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts +++ b/packages/nextjs/test/common/wrapServerComponentWithSentry.test.ts @@ -40,6 +40,8 @@ describe('wrapServerComponentWithSentry', () => { it.each([ ['not-found', 'NEXT_NOT_FOUND'], ['redirect', 'NEXT_REDIRECT;/somewhere'], + ['forbidden', 'NEXT_HTTP_ERROR_FALLBACK;403'], + ['unauthorized', 'NEXT_HTTP_ERROR_FALLBACK;401'], ['hanging prerender promise', 'HANGING_PROMISE_REJECTION'], ['prerender interruption', 'NEXT_PRERENDER_INTERRUPTED'], ['dynamic server usage', 'DYNAMIC_SERVER_USAGE'], @@ -53,6 +55,18 @@ describe('wrapServerComponentWithSentry', () => { expect(captureException).not.toHaveBeenCalled(); }); + it.each([ + ['NEXT_HTTP_ERROR_FALLBACK;403', 'permission_denied'], + ['NEXT_HTTP_ERROR_FALLBACK;401', 'unauthenticated'], + ])('marks the active span for the %s auth interrupt', async (digest, message) => { + vi.spyOn(SentryCore, 'captureException').mockImplementation(() => ''); + const { setStatus } = mockActiveSpan({} as Span); + + await runWrapped(Object.assign(new Error('control flow'), { digest })); + + expect(setStatus).toHaveBeenCalledWith({ code: SentryCore.SPAN_STATUS_ERROR, message }); + }); + it.each(['NEXT_NOT_FOUND', 'NEXT_REDIRECT;/somewhere', 'HANGING_PROMISE_REJECTION'])( 'does not capture the %s control flow error when there is no active span', async digest => { From d441a34b781903d8a13d3e251a7c88ed6676b9c2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 7 Oct 2026 11:06:02 +0200 Subject: [PATCH 70/73] test(nextjs): Expect use cache route handler file path on Turbopack (#25117) Next.js 16.4 Turbopack now lists route handler `use cache` functions in the server-reference manifest, so `code.file.path` is set on `cache.put` spans for both bundlers. This drops the webpack-only expectation in both cacheComponents E2E apps, which started failing once CI resolved `next@^16` to 16.4.0. Co-authored-by: Claude Opus 5.5 --- .../nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts | 8 +------- .../tests/useCacheSpans.spec.ts | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts index 9b93cef32218..2a5a6c44c9dc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/useCacheSpans.spec.ts @@ -1,12 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -// Webpack lists route handler `use cache` functions in the server-reference manifest, Turbopack -// (Next.js 16.3) does not. `TEST_BUNDLER` marks the webpack variant's production run (set in -// `test:assert-webpack`). -const isWebpackBuild = process.env.TEST_BUNDLER === 'webpack' || process.env.TEST_ENV === 'development-webpack'; -const expectedRouteHandlerFilePath = isWebpackBuild ? 'app/api/use-cache/route.ts' : undefined; - test('Should create cache spans around `use cache` functions', async ({ request }) => { // A fresh id makes the first request a guaranteed cache miss (the id is part of the cache key) // even when the test is retried against the same server. @@ -63,7 +57,7 @@ test('Should create cache spans around `use cache` functions', async ({ request }), }); - expect(putSpan!.data?.['code.file.path']).toBe(expectedRouteHandlerFilePath); + expect(putSpan!.data?.['code.file.path']).toBe('app/api/use-cache/route.ts'); const hitGetSpan = hitTx.spans?.find(span => span.op === 'cache.get'); expect(hitGetSpan).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts index c4f7a5c5b8ca..cca7ad2a9e63 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/useCacheSpans.spec.ts @@ -2,12 +2,6 @@ import { expect, test } from '@playwright/test'; import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; import { findCacheSpan } from './cacheOriginLinks-utils'; -// Webpack lists route handler `use cache` functions in the server-reference manifest, Turbopack -// (Next.js 16.3) does not. `TEST_BUNDLER` marks the webpack variant's production run (set in -// `test:assert-webpack`). -const isWebpackBuild = process.env.TEST_BUNDLER === 'webpack' || process.env.TEST_ENV === 'development-webpack'; -const expectedRouteHandlerFilePath = isWebpackBuild ? 'app/api/use-cache/route.ts' : undefined; - test('uses low-cardinality names for `use cache` spans', async ({ request }) => { // A fresh id makes the request a guaranteed cache miss (the id is part of the cache key), so the // trace contains both a `cache.get` and a `cache.put` span. @@ -37,7 +31,7 @@ test('uses low-cardinality names for `use cache` spans', async ({ request }) => expect(putSpan!.name).toBe('cache.put'); expect(putSpan!.attributes['cache.key']?.value).toEqual(cacheKeyDigest); - expect(putSpan!.attributes['code.file.path']?.value).toBe(expectedRouteHandlerFilePath); + expect(putSpan!.attributes['code.file.path']?.value).toBe('app/api/use-cache/route.ts'); }); test('sets the source file of the cached component on `cache.put` spans', async ({ request }) => { From dd398d04c3116c246ab7ab18749c55c54fcb0d6c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 7 Oct 2026 11:08:53 +0200 Subject: [PATCH 71/73] test(nextjs): Pin nextjs-16-cf-workers latest variant to Next.js 16.3 (#25118) `@opennextjs/cloudflare` throws on the new `preview-props.json` manifest that Next.js 16.4 loads, so every request in the `(latest)` variant returns a 500 (https://github.com/opennextjs/opennextjs-cloudflare/issues/1355). This pins the variant to 16.3 until the upstream fix (https://github.com/opennextjs/opennextjs-cloudflare/pull/1356) is released, and we should switch it back to `next@latest` then. Co-authored-by: Claude Opus 5.5 --- .../test-applications/nextjs-16-cf-workers/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json index 6de585c71e76..2bd1b3f77857 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json @@ -13,7 +13,7 @@ "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm cf:build", "test:build-canary": "pnpm install && pnpm add next@canary && pnpm cf:build", - "test:build-latest": "pnpm install && pnpm add next@latest && pnpm cf:build", + "test:build-latest": "pnpm install && pnpm add next@16.3 && pnpm cf:build", "test:assert": "pnpm test:prod" }, "dependencies": { From 603eb60a651653fe47f6ffff6e998dce57f1c41a Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 7 Oct 2026 11:31:09 +0200 Subject: [PATCH 72/73] fix(ai): Normalize token usage and preserve cache breakdowns (#25074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cached-token usage was inconsistent across AI integrations: Anthropic reported only uncached input tokens, some integrations dropped cache breakdowns, and LangChain could overwrite complete usage with counters from the final stream chunk. This produced incomplete totals and could leave cache counts greater than the reported input count, breaking downstream pricing. This PR treats input and output counts as inclusive totals. Cache reads and writes are subsets of input, and reasoning tokens are a subset of output. Provider-specific normalization happens before setting those attributes, so the breakdowns are never added to an already-inclusive total. - **Anthropic:** Include cache reads and writes in input usage and emit both breakdown attributes for regular and streaming responses. - **OpenAI:** Preserve cache details from Chat Completions and Responses usage without increasing their existing totals. - **Google GenAI:** Preserve cached-input usage and include thinking tokens in output, with reasoning reported separately. - **LangChain:** Prefer normalized message usage over generic `tokenUsage`, which can contain only the final stream chunk’s counters. Normalize raw Anthropic usage as a fallback and count shared candidate usage once per prompt. - **LangGraph:** Aggregate cache breakdowns alongside token totals for newly generated messages, excluding conversation history. Regression coverage includes competing usage sources, repeated streaming events, missing and zero cache counts, batched prompts, and shared candidate usage. Fixes #25069 --------- Co-authored-by: GPT-6 Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- .../server-utils/src/ai/anthropic-ai/index.ts | 17 +- .../src/ai/anthropic-ai/streaming.ts | 7 +- .../server-utils/src/ai/anthropic-ai/types.ts | 4 +- packages/server-utils/src/ai/core/utils.ts | 93 +++--- .../server-utils/src/ai/google-genai/index.ts | 32 +-- .../src/ai/google-genai/streaming.ts | 10 +- .../server-utils/src/ai/langchain/types.ts | 15 + .../server-utils/src/ai/langchain/utils.ts | 119 +++++--- .../server-utils/src/ai/langgraph/utils.ts | 70 ++--- .../server-utils/src/ai/openai/streaming.ts | 10 + packages/server-utils/src/ai/openai/types.ts | 2 + packages/server-utils/src/ai/openai/utils.ts | 12 + .../server-utils/src/ai/workers-ai/utils.ts | 4 +- .../ai/lib/tracing/anthropic-ai-usage.test.ts | 126 +++++++++ .../ai/lib/tracing/cache-token-usage.test.ts | 186 ++++++++++++ .../lib/tracing/langchain-token-usage.test.ts | 264 ++++++++++++++++++ .../test/ai/lib/utils/langgraph-utils.test.ts | 66 ++++- 17 files changed, 879 insertions(+), 158 deletions(-) create mode 100644 packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts create mode 100644 packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts create mode 100644 packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts diff --git a/packages/server-utils/src/ai/anthropic-ai/index.ts b/packages/server-utils/src/ai/anthropic-ai/index.ts index c285a043bc4c..173ff3c638d4 100644 --- a/packages/server-utils/src/ai/anthropic-ai/index.ts +++ b/packages/server-utils/src/ai/anthropic-ai/index.ts @@ -21,8 +21,8 @@ import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; import { getGenAiSpanOp, + getTokenUsageAttributes, resolveAIRecordingOptions, - setTokenUsageAttributes, wrapPromiseWithMethods, } from '../core/utils'; import { ANTHROPIC_METHOD_REGISTRY } from './constants'; @@ -125,12 +125,15 @@ function addMetadataAttributes(span: Span, response: AnthropicAiResponse): void }); if ('usage' in response && response.usage) { - setTokenUsageAttributes( - span, - response.usage.input_tokens, - response.usage.output_tokens, - response.usage.cache_creation_input_tokens, - response.usage.cache_read_input_tokens, + span.setAttributes( + getTokenUsageAttributes( + response.usage.input_tokens + + (response.usage.cache_creation_input_tokens ?? 0) + + (response.usage.cache_read_input_tokens ?? 0), + response.usage.output_tokens, + response.usage.cache_creation_input_tokens, + response.usage.cache_read_input_tokens, + ), ); } } diff --git a/packages/server-utils/src/ai/anthropic-ai/streaming.ts b/packages/server-utils/src/ai/anthropic-ai/streaming.ts index 3daf61c2b57e..20507ed4f3bf 100644 --- a/packages/server-utils/src/ai/anthropic-ai/streaming.ts +++ b/packages/server-utils/src/ai/anthropic-ai/streaming.ts @@ -83,7 +83,12 @@ function handleMessageMetadata(event: AnthropicAiStreamingEvent, state: Streamin if (message.model) state.responseModel = message.model; if (message.usage) { - if (typeof message.usage.input_tokens === 'number') state.promptTokens = message.usage.input_tokens; + if (typeof message.usage.input_tokens === 'number') { + state.promptTokens = + message.usage.input_tokens + + (message.usage.cache_creation_input_tokens ?? 0) + + (message.usage.cache_read_input_tokens ?? 0); + } if (typeof message.usage.cache_creation_input_tokens === 'number') state.cacheCreationInputTokens = message.usage.cache_creation_input_tokens; if (typeof message.usage.cache_read_input_tokens === 'number') diff --git a/packages/server-utils/src/ai/anthropic-ai/types.ts b/packages/server-utils/src/ai/anthropic-ai/types.ts index 1714349dcc48..bb768e63063e 100644 --- a/packages/server-utils/src/ai/anthropic-ai/types.ts +++ b/packages/server-utils/src/ai/anthropic-ai/types.ts @@ -40,8 +40,8 @@ type SuccessfulResponse = { usage?: { input_tokens: number; output_tokens: number; - cache_creation_input_tokens: number; - cache_read_input_tokens: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; }; error?: never; // This should help TypeScript infer the type correctly }; diff --git a/packages/server-utils/src/ai/core/utils.ts b/packages/server-utils/src/ai/core/utils.ts index d83c00edfd7f..49f76fb18998 100644 --- a/packages/server-utils/src/ai/core/utils.ts +++ b/packages/server-utils/src/ai/core/utils.ts @@ -12,8 +12,11 @@ import { GEN_AI_RESPONSE_STREAMING, GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; import { FUNCTION } from '@sentry/conventions/op'; @@ -100,47 +103,37 @@ export function isReadableStream(value: unknown): value is Readable } /** - * 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 + * Build token usage attributes. Input tokens include cache reads and writes, which are subsets. */ -export function setTokenUsageAttributes( - span: Span, +export function getTokenUsageAttributes( promptTokens?: number, completionTokens?: number, - cachedInputTokens?: number, - cachedOutputTokens?: number, -): void { - if (promptTokens !== undefined) { - span.setAttributes({ - [GEN_AI_USAGE_INPUT_TOKENS]: promptTokens, - }); + cacheCreationInputTokens?: number, + cacheReadInputTokens?: number, + totalTokens?: number, +): Record { + const attributes: Record = {}; + + if (typeof promptTokens === 'number') { + attributes[GEN_AI_USAGE_INPUT_TOKENS] = promptTokens; } - if (completionTokens !== undefined) { - span.setAttributes({ - [GEN_AI_USAGE_OUTPUT_TOKENS]: completionTokens, - }); + if (typeof cacheCreationInputTokens === 'number') { + attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = cacheCreationInputTokens; } - 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, - }); + if (typeof cacheReadInputTokens === 'number') { + attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = cacheReadInputTokens; } + if (typeof completionTokens === 'number') { + attributes[GEN_AI_USAGE_OUTPUT_TOKENS] = completionTokens; + } + if (typeof totalTokens === 'number') { + attributes[GEN_AI_USAGE_TOTAL_TOKENS] = totalTokens; + } else if (typeof promptTokens === 'number' || typeof completionTokens === 'number') { + attributes[GEN_AI_USAGE_TOTAL_TOKENS] = + (typeof promptTokens === 'number' ? promptTokens : 0) + + (typeof completionTokens === 'number' ? completionTokens : 0); + } + return attributes; } /** One assistant turn for {@link setOutputMessagesAttribute}. */ @@ -227,6 +220,7 @@ export interface StreamResponseState { totalTokens?: number; cacheCreationInputTokens?: number; cacheReadInputTokens?: number; + reasoningOutputTokens?: number; } /** @@ -245,23 +239,18 @@ export function endStreamSpan(span: Span, state: StreamResponseState, recordOutp 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); + Object.assign( + attrs, + getTokenUsageAttributes( + state.promptTokens, + state.completionTokens, + state.cacheCreationInputTokens, + state.cacheReadInputTokens, + state.totalTokens, + ), + ); + if (typeof state.reasoningOutputTokens === 'number') { + attrs[GEN_AI_USAGE_REASONING_OUTPUT_TOKENS] = state.reasoningOutputTokens; } if (state.finishReasons.length) { diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index 06bd425bc188..16dbb01d98f9 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -27,13 +27,11 @@ import { GEN_AI_RESPONSE_TOOL_CALLS, GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_TOOL_DEFINITIONS, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; -import { buildMethodPath, getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; +import { buildMethodPath, getGenAiSpanOp, getTokenUsageAttributes, resolveAIRecordingOptions } from '../core/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; @@ -205,20 +203,18 @@ export function addResponseAttributes(span: Span, response: GoogleGenAIResponse, // Add usage metadata if present if (response.usageMetadata && typeof response.usageMetadata === 'object') { const usage = response.usageMetadata; - if (typeof usage.promptTokenCount === 'number') { - span.setAttributes({ - [GEN_AI_USAGE_INPUT_TOKENS]: usage.promptTokenCount, - }); - } - if (typeof usage.candidatesTokenCount === 'number') { - span.setAttributes({ - [GEN_AI_USAGE_OUTPUT_TOKENS]: usage.candidatesTokenCount, - }); - } - if (typeof usage.totalTokenCount === 'number') { - span.setAttributes({ - [GEN_AI_USAGE_TOTAL_TOKENS]: usage.totalTokenCount, - }); + const hasOutput = typeof usage.candidatesTokenCount === 'number' || typeof usage.thoughtsTokenCount === 'number'; + span.setAttributes( + getTokenUsageAttributes( + usage.promptTokenCount, + hasOutput ? (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0) : undefined, + undefined, + usage.cachedContentTokenCount, + usage.totalTokenCount, + ), + ); + if (typeof usage.thoughtsTokenCount === 'number') { + span.setAttribute(GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, usage.thoughtsTokenCount); } } diff --git a/packages/server-utils/src/ai/google-genai/streaming.ts b/packages/server-utils/src/ai/google-genai/streaming.ts index 4937b0742b3e..3a2f720f2628 100644 --- a/packages/server-utils/src/ai/google-genai/streaming.ts +++ b/packages/server-utils/src/ai/google-genai/streaming.ts @@ -21,8 +21,11 @@ interface StreamingState { promptTokens?: number; /** Number of completion/output tokens used. */ completionTokens?: number; + candidateTokens?: number; + reasoningOutputTokens?: number; /** Number of total tokens used. */ totalTokens?: number; + cacheReadInputTokens?: number; /** Accumulated tool calls (finalized) */ toolCalls: Array>; /** Accumulated output message parts (for output recording). */ @@ -60,8 +63,13 @@ function handleResponseMetadata(chunk: GoogleGenAIResponse, state: StreamingStat const usage = chunk.usageMetadata; if (usage) { if (typeof usage.promptTokenCount === 'number') state.promptTokens = usage.promptTokenCount; - if (typeof usage.candidatesTokenCount === 'number') state.completionTokens = usage.candidatesTokenCount; + if (typeof usage.candidatesTokenCount === 'number') state.candidateTokens = usage.candidatesTokenCount; + if (typeof usage.thoughtsTokenCount === 'number') state.reasoningOutputTokens = usage.thoughtsTokenCount; + if (state.candidateTokens !== undefined || state.reasoningOutputTokens !== undefined) { + state.completionTokens = (state.candidateTokens ?? 0) + (state.reasoningOutputTokens ?? 0); + } if (typeof usage.totalTokenCount === 'number') state.totalTokens = usage.totalTokenCount; + if (typeof usage.cachedContentTokenCount === 'number') state.cacheReadInputTokens = usage.cachedContentTokenCount; } } diff --git a/packages/server-utils/src/ai/langchain/types.ts b/packages/server-utils/src/ai/langchain/types.ts index 352aecdea295..03039782c5bf 100644 --- a/packages/server-utils/src/ai/langchain/types.ts +++ b/packages/server-utils/src/ai/langchain/types.ts @@ -41,12 +41,27 @@ export interface LangChainMessage { }; role?: string; additional_kwargs?: Record; + usage_metadata?: { + input_tokens?: number; + output_tokens?: number; + total_tokens?: number; + input_token_details?: { + cache_read?: number; + cache_creation?: number; + }; + }; // LangChain serialized format lc?: number; id?: string[] | string; response_metadata?: { + [key: string]: unknown; model_name?: string; finish_reason?: string; + tokenUsage?: { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + }; }; kwargs?: { [key: string]: unknown; diff --git a/packages/server-utils/src/ai/langchain/utils.ts b/packages/server-utils/src/ai/langchain/utils.ts index 88104ee87038..067a82fc2d75 100644 --- a/packages/server-utils/src/ai/langchain/utils.ts +++ b/packages/server-utils/src/ai/langchain/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ /* eslint-disable typescript-eslint/no-deprecated */ -import { stringify } from '@sentry/core'; +import { isObjectLike, stringify } from '@sentry/core'; import type { SpanAttributeValue } from '@sentry/core'; import { GEN_AI_AGENT_NAME, @@ -22,13 +22,10 @@ import { GEN_AI_SYSTEM_INSTRUCTIONS, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, - GEN_AI_USAGE_INPUT_TOKENS, - GEN_AI_USAGE_OUTPUT_TOKENS, - GEN_AI_USAGE_TOTAL_TOKENS, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE, GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../core/gen-ai-attributes'; -import { extractSystemInstructions } from '../core/utils'; +import { extractSystemInstructions, getTokenUsageAttributes } from '../core/utils'; import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants'; import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types'; @@ -330,16 +327,46 @@ function addToolCallsAttributes(generations: LangChainMessage[][], attrs: Record } } -/** - * Adds token usage attributes, supporting both OpenAI (`tokenUsage`) and Anthropic (`usage`) formats. - * - Preserve zero values (0 tokens) by avoiding truthy checks. - * - Compute a total for Anthropic when not explicitly provided. - * - Include cache token metrics when present. - */ -function addTokenUsageAttributes( - llmOutput: LangChainLLMResult['llmOutput'], - attrs: Record, -): void { +export function extractMessageTokenUsageAttributes(message: LangChainMessage): Record { + const usage = message.usage_metadata; + if (isObjectLike(usage)) { + const details = isObjectLike(usage.input_token_details) ? usage.input_token_details : undefined; + return getTokenUsageAttributes( + usage.input_tokens, + usage.output_tokens, + details?.cache_creation, + details?.cache_read, + usage.total_tokens, + ); + } + + const metadata = message.response_metadata; + const tokenUsage = isObjectLike(metadata) && isObjectLike(metadata.tokenUsage) ? metadata.tokenUsage : undefined; + return getTokenUsageAttributes( + tokenUsage?.promptTokens, + tokenUsage?.completionTokens, + undefined, + undefined, + tokenUsage?.totalTokens, + ); +} + +function addTokenUsageAttributes(llmResult: LangChainLLMResult, attrs: Record): void { + const normalizedUsage: Record = {}; + const legacyMessageUsage: Record = {}; + for (const generations of llmResult.generations ?? []) { + // Candidate messages share the usage totals for their prompt. + const message = generations[0]?.message; + if (message) { + const usage = isObjectLike(message.usage_metadata) ? normalizedUsage : legacyMessageUsage; + for (const [key, value] of Object.entries(extractMessageTokenUsageAttributes(message))) { + usage[key] = (usage[key] ?? 0) + value; + } + } + } + Object.assign(attrs, Object.keys(normalizedUsage).length > 0 ? normalizedUsage : legacyMessageUsage); + + const llmOutput = llmResult.llmOutput; if (!llmOutput) return; const tokenUsage = llmOutput.tokenUsage; @@ -352,25 +379,47 @@ function addTokenUsageAttributes( } | undefined; - if (tokenUsage) { - setNumberIfDefined(attrs, GEN_AI_USAGE_INPUT_TOKENS, tokenUsage.promptTokens); - setNumberIfDefined(attrs, GEN_AI_USAGE_OUTPUT_TOKENS, tokenUsage.completionTokens); - setNumberIfDefined(attrs, GEN_AI_USAGE_TOTAL_TOKENS, tokenUsage.totalTokens); - } else if (anthropicUsage) { - setNumberIfDefined(attrs, GEN_AI_USAGE_INPUT_TOKENS, anthropicUsage.input_tokens); - setNumberIfDefined(attrs, GEN_AI_USAGE_OUTPUT_TOKENS, anthropicUsage.output_tokens); - - // Compute total when not provided by the provider. - const input = Number(anthropicUsage.input_tokens); - const output = Number(anthropicUsage.output_tokens); - const total = (Number.isNaN(input) ? 0 : input) + (Number.isNaN(output) ? 0 : output); - if (total > 0) setNumberIfDefined(attrs, GEN_AI_USAGE_TOTAL_TOKENS, total); - - // Extra Anthropic cache metrics (present only when caching is enabled) - if (anthropicUsage.cache_creation_input_tokens !== undefined) - setNumberIfDefined(attrs, GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, anthropicUsage.cache_creation_input_tokens); - if (anthropicUsage.cache_read_input_tokens !== undefined) - setNumberIfDefined(attrs, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, anthropicUsage.cache_read_input_tokens); + // llmOutput.tokenUsage can contain only the final stream chunk's counters. + if (anthropicUsage && Object.keys(normalizedUsage).length === 0 && Object.keys(legacyMessageUsage).length === 0) { + const { input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens } = anthropicUsage; + const hasInput = + input_tokens !== undefined || cache_creation_input_tokens !== undefined || cache_read_input_tokens !== undefined; + Object.assign( + attrs, + getTokenUsageAttributes( + hasInput + ? (input_tokens ?? 0) + (cache_creation_input_tokens ?? 0) + (cache_read_input_tokens ?? 0) + : undefined, + output_tokens, + cache_creation_input_tokens, + cache_read_input_tokens, + ), + ); + } else if (tokenUsage && Object.keys(normalizedUsage).length === 0 && Object.keys(legacyMessageUsage).length === 0) { + Object.assign( + attrs, + getTokenUsageAttributes( + tokenUsage.promptTokens, + tokenUsage.completionTokens, + undefined, + undefined, + tokenUsage.totalTokens, + ), + ); + } + if (anthropicUsage) { + if ( + typeof anthropicUsage.cache_creation_input_tokens === 'number' && + !(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS in attrs) + ) { + attrs[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = anthropicUsage.cache_creation_input_tokens; + } + if ( + typeof anthropicUsage.cache_read_input_tokens === 'number' && + !(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS in attrs) + ) { + attrs[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = anthropicUsage.cache_read_input_tokens; + } } } @@ -426,7 +475,7 @@ export function extractLlmResponseAttributes( } } - addTokenUsageAttributes(llmResult.llmOutput, attrs); + addTokenUsageAttributes(llmResult, attrs); const llmOutput = llmResult.llmOutput; diff --git a/packages/server-utils/src/ai/langgraph/utils.ts b/packages/server-utils/src/ai/langgraph/utils.ts index d8fedd2dbb0e..dd6d7e61358e 100644 --- a/packages/server-utils/src/ai/langgraph/utils.ts +++ b/packages/server-utils/src/ai/langgraph/utils.ts @@ -12,6 +12,8 @@ import { GEN_AI_TOOL_CALL_RESULT, GEN_AI_TOOL_DESCRIPTION, GEN_AI_TOOL_NAME, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, @@ -21,7 +23,11 @@ import { import { GEN_AI_EXECUTE_TOOL } from '@sentry/conventions/op'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { BaseChatModel, LangChainMessage } from '../langchain/types'; -import { getConversationIdFromMetadata, normalizeLangChainMessages } from '../langchain/utils'; +import { + extractMessageTokenUsageAttributes, + getConversationIdFromMetadata, + normalizeLangChainMessages, +} from '../langchain/utils'; import { LANGGRAPH_ORIGIN } from './constants'; import type { CompiledGraph, LangGraphOptions, LangGraphTool } from './types'; @@ -185,45 +191,17 @@ export function extractTokenUsageFromMessage(message: LangChainMessage): { inputTokens: number; outputTokens: number; totalTokens: number; + cacheCreationInputTokens?: number; + cacheReadInputTokens?: number; } { - const msg = message as Record; - let inputTokens = 0; - let outputTokens = 0; - let totalTokens = 0; - - // Extract from usage_metadata (newer format) - if (msg.usage_metadata && typeof msg.usage_metadata === 'object') { - const usage = msg.usage_metadata as Record; - if (typeof usage.input_tokens === 'number') { - inputTokens = usage.input_tokens; - } - if (typeof usage.output_tokens === 'number') { - outputTokens = usage.output_tokens; - } - if (typeof usage.total_tokens === 'number') { - totalTokens = usage.total_tokens; - } - return { inputTokens, outputTokens, totalTokens }; - } - - // Fallback: Extract from response_metadata.tokenUsage - if (msg.response_metadata && typeof msg.response_metadata === 'object') { - const metadata = msg.response_metadata as Record; - if (metadata.tokenUsage && typeof metadata.tokenUsage === 'object') { - const tokenUsage = metadata.tokenUsage as Record; - if (typeof tokenUsage.promptTokens === 'number') { - inputTokens = tokenUsage.promptTokens; - } - if (typeof tokenUsage.completionTokens === 'number') { - outputTokens = tokenUsage.completionTokens; - } - if (typeof tokenUsage.totalTokens === 'number') { - totalTokens = tokenUsage.totalTokens; - } - } - } - - return { inputTokens, outputTokens, totalTokens }; + const attributes = extractMessageTokenUsageAttributes(message); + return { + inputTokens: attributes[GEN_AI_USAGE_INPUT_TOKENS] ?? 0, + outputTokens: attributes[GEN_AI_USAGE_OUTPUT_TOKENS] ?? 0, + totalTokens: attributes[GEN_AI_USAGE_TOTAL_TOKENS] ?? 0, + cacheCreationInputTokens: attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS], + cacheReadInputTokens: attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS], + }; } /** @@ -304,6 +282,8 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag let totalInputTokens = 0; let totalOutputTokens = 0; let totalTokens = 0; + let cacheCreationInputTokens: number | undefined; + let cacheReadInputTokens: number | undefined; // Extract metadata from messages for (const message of newMessages) { @@ -312,6 +292,12 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag totalInputTokens += tokens.inputTokens; totalOutputTokens += tokens.outputTokens; totalTokens += tokens.totalTokens; + if (tokens.cacheCreationInputTokens !== undefined) { + cacheCreationInputTokens = (cacheCreationInputTokens ?? 0) + tokens.cacheCreationInputTokens; + } + if (tokens.cacheReadInputTokens !== undefined) { + cacheReadInputTokens = (cacheReadInputTokens ?? 0) + tokens.cacheReadInputTokens; + } // Extract model metadata (last message's metadata wins for model/finish_reason) extractModelMetadata(span, message); @@ -327,4 +313,10 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag if (totalTokens > 0) { span.setAttribute(GEN_AI_USAGE_TOTAL_TOKENS, totalTokens); } + if (cacheCreationInputTokens !== undefined) { + span.setAttribute(GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, cacheCreationInputTokens); + } + if (cacheReadInputTokens !== undefined) { + span.setAttribute(GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cacheReadInputTokens); + } } diff --git a/packages/server-utils/src/ai/openai/streaming.ts b/packages/server-utils/src/ai/openai/streaming.ts index 66b5e11491f6..0828e755e4cb 100644 --- a/packages/server-utils/src/ai/openai/streaming.ts +++ b/packages/server-utils/src/ai/openai/streaming.ts @@ -31,6 +31,8 @@ interface StreamingState { completionTokens: number | undefined; /** Total number of tokens used (prompt + completion). */ totalTokens: number | undefined; + cacheReadInputTokens: number | undefined; + cacheCreationInputTokens: number | undefined; /** * Accumulated tool calls from Chat Completion streaming, indexed by tool call index. * @see https://platform.openai.com/docs/guides/function-calling?api-mode=chat#streaming @@ -96,6 +98,9 @@ function processChatCompletionChunk(chunk: ChatCompletionChunk, state: Streaming state.promptTokens = chunk.usage.prompt_tokens; state.completionTokens = chunk.usage.completion_tokens; state.totalTokens = chunk.usage.total_tokens; + state.cacheReadInputTokens = chunk.usage.prompt_tokens_details?.cached_tokens ?? state.cacheReadInputTokens; + state.cacheCreationInputTokens = + chunk.usage.prompt_tokens_details?.cache_write_tokens ?? state.cacheCreationInputTokens; } for (const choice of chunk.choices ?? []) { @@ -179,6 +184,9 @@ function processResponsesApiEvent( state.promptTokens = response.usage.input_tokens; state.completionTokens = response.usage.output_tokens; state.totalTokens = response.usage.total_tokens; + state.cacheReadInputTokens = response.usage.input_tokens_details?.cached_tokens ?? state.cacheReadInputTokens; + state.cacheCreationInputTokens = + response.usage.input_tokens_details?.cache_write_tokens ?? state.cacheCreationInputTokens; } if (response.status) { @@ -215,6 +223,8 @@ export async function* instrumentStream( promptTokens: undefined, completionTokens: undefined, totalTokens: undefined, + cacheReadInputTokens: undefined, + cacheCreationInputTokens: undefined, chatCompletionToolCalls: {}, responsesApiToolCalls: [], }; diff --git a/packages/server-utils/src/ai/openai/types.ts b/packages/server-utils/src/ai/openai/types.ts index efc62b6e967a..cb57ea84b7a0 100644 --- a/packages/server-utils/src/ai/openai/types.ts +++ b/packages/server-utils/src/ai/openai/types.ts @@ -49,6 +49,7 @@ export interface OpenAIResponseObject { input_tokens: number; input_tokens_details?: { cached_tokens?: number; + cache_write_tokens?: number; }; output_tokens: number; output_tokens_details?: { @@ -231,6 +232,7 @@ export interface ChatCompletionChunk { prompt_tokens_details: { audio_tokens: number; cached_tokens: number; + cache_write_tokens?: number; }; }; } diff --git a/packages/server-utils/src/ai/openai/utils.ts b/packages/server-utils/src/ai/openai/utils.ts index 429340530e2c..49b39eb85013 100644 --- a/packages/server-utils/src/ai/openai/utils.ts +++ b/packages/server-utils/src/ai/openai/utils.ts @@ -1,4 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated */ +import { isObjectLike } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_CONVERSATION_ID, @@ -12,6 +13,8 @@ import { GEN_AI_RESPONSE_MODEL, GEN_AI_RESPONSE_TEXT, GEN_AI_RESPONSE_TOOL_CALLS, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, @@ -90,6 +93,15 @@ export function addResponseAttributes(span: Span, result: unknown, recordOutputs if (typeof usage.total_tokens === 'number') { attrs[GEN_AI_USAGE_TOTAL_TOKENS] = usage.total_tokens; } + const inputDetails = usage.prompt_tokens_details ?? usage.input_tokens_details; + if (isObjectLike(inputDetails)) { + if (typeof inputDetails.cached_tokens === 'number') { + attrs[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = inputDetails.cached_tokens; + } + if (typeof inputDetails.cache_write_tokens === 'number') { + attrs[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = inputDetails.cache_write_tokens; + } + } } // Finish reasons from choices (chat completions) diff --git a/packages/server-utils/src/ai/workers-ai/utils.ts b/packages/server-utils/src/ai/workers-ai/utils.ts index e2ef7be9c438..66dd3d567c8c 100644 --- a/packages/server-utils/src/ai/workers-ai/utils.ts +++ b/packages/server-utils/src/ai/workers-ai/utils.ts @@ -20,7 +20,7 @@ import { GEN_AI_CHAT, GEN_AI_EMBEDDINGS, GEN_AI_EVALUATE } from '@sentry/convent import { isObjectLike, stringify } from '@sentry/core'; import type { Span, SpanAttributeValue } from '@sentry/core'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; -import { extractSystemInstructions, setOutputMessagesAttribute, setTokenUsageAttributes } from '../core/utils'; +import { extractSystemInstructions, getTokenUsageAttributes, setOutputMessagesAttribute } from '../core/utils'; import { addResponseAttributes as addEvaluateResponseAttributes, getEvaluationInputMessages } from '../typesafe'; // Re-exported so `workers-ai/streaming.ts` keeps importing it from this module. export { setOutputMessagesAttribute }; @@ -173,7 +173,7 @@ export function addResponseAttributes( const response = result as WorkersAiOutput; if (response.usage) { - setTokenUsageAttributes(span, response.usage.prompt_tokens, response.usage.completion_tokens); + span.setAttributes(getTokenUsageAttributes(response.usage.prompt_tokens, response.usage.completion_tokens)); } if (recordOutputs) { diff --git a/packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts b/packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts new file mode 100644 index 000000000000..aa62ff8d78ba --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/anthropic-ai-usage.test.ts @@ -0,0 +1,126 @@ +import { + GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { EventEmitter } from 'node:events'; +import { describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { addResponseAttributes } from '../../../../src/ai/anthropic-ai'; +import { instrumentAsyncIterableStream, instrumentMessageStream } from '../../../../src/ai/anthropic-ai/streaming'; +import type { AnthropicAiStreamingEvent } from '../../../../src/ai/anthropic-ai/types'; + +function createMockSpan(): { span: Span; attributes: Record } { + const attributes: Record = {}; + let ended = false; + const span = { + isRecording: () => !ended, + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + setAttributes: (values: Record) => { + Object.assign(attributes, values); + }, + end: () => { + ended = true; + }, + } as unknown as Span; + return { span, attributes }; +} + +describe.each(['response', 'async iterable', 'message stream'] as const)('Anthropic token usage (%s)', mode => { + it.each([ + { + name: 'includes cache reads and writes in input tokens', + cache: { cache_read_input_tokens: 2048, cache_creation_input_tokens: 512 }, + inputTokens: 2600, + totalTokens: 2720, + cacheAttributes: { + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 512, + }, + }, + { + name: 'includes cache reads when cache writes are absent', + cache: { cache_read_input_tokens: 2048 }, + inputTokens: 2088, + totalTokens: 2208, + cacheAttributes: { [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048 }, + }, + { + name: 'includes cache writes when cache reads are absent', + cache: { cache_creation_input_tokens: 512 }, + inputTokens: 552, + totalTokens: 672, + cacheAttributes: { [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 512 }, + }, + { + name: 'preserves zero cache counts', + cache: { cache_read_input_tokens: 0, cache_creation_input_tokens: 0 }, + inputTokens: 40, + totalTokens: 160, + cacheAttributes: { + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 0, + }, + }, + { + name: 'omits cache attributes when cache usage is absent', + cache: {}, + inputTokens: 40, + totalTokens: 160, + cacheAttributes: {}, + }, + ])('$name', async ({ cache, inputTokens, totalTokens, cacheAttributes }) => { + const { span, attributes } = createMockSpan(); + const response = { + id: 'msg_cache_usage', + type: 'message' as const, + model: 'claude-sonnet-4-6', + role: 'assistant', + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 40, output_tokens: 120, ...cache }, + }; + const events: AnthropicAiStreamingEvent[] = [ + { type: 'message_start', message: { ...response, usage: { ...response.usage, output_tokens: 1 } } }, + { type: 'message_delta', usage: { output_tokens: 60 } }, + { type: 'message_delta', usage: { output_tokens: 120 } }, + { type: 'message_stop' }, + ]; + + if (mode === 'response') { + addResponseAttributes(span, response, false); + } else if (mode === 'async iterable') { + const stream = (async function* () { + yield* events; + })(); + for await (const _ of instrumentAsyncIterableStream(stream, span, false)) { + void _; + } + } else { + const stream = instrumentMessageStream(new EventEmitter(), span, false); + for (const event of events) { + stream.emit('streamEvent', event); + } + stream.emit('message', response); + stream.emit('message', response); + } + + expect(attributes).toEqual({ + [GEN_AI_RESPONSE_ID]: 'msg_cache_usage', + [GEN_AI_RESPONSE_MODEL]: 'claude-sonnet-4-6', + [GEN_AI_USAGE_INPUT_TOKENS]: inputTokens, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, + ...cacheAttributes, + ...(mode === 'response' ? {} : { [GEN_AI_RESPONSE_STREAMING]: true }), + }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts b/packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts new file mode 100644 index 000000000000..6e88ac2f7421 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/cache-token-usage.test.ts @@ -0,0 +1,186 @@ +import { + GEN_AI_RESPONSE_ID, + GEN_AI_RESPONSE_MODEL, + GEN_AI_RESPONSE_STREAMING, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { describe, expect, it } from 'vitest'; +import type { Span } from '@sentry/core'; +import { addResponseAttributes as addOpenAiResponseAttributes } from '../../../../src/ai/openai/utils'; +import { instrumentStream as instrumentOpenAiStream } from '../../../../src/ai/openai/streaming'; +import { addResponseAttributes as addGoogleResponseAttributes } from '../../../../src/ai/google-genai'; +import { instrumentStream as instrumentGoogleStream } from '../../../../src/ai/google-genai/streaming'; + +describe.each(['openai chat', 'openai responses', 'google'] as const)('cache usage (%s)', provider => { + describe.each(['response', 'stream'] as const)('%s', mode => { + it.each([ + { name: 'records cache subsets without increasing input or total tokens', cacheRead: 2048, cacheWrite: 512 }, + { name: 'preserves zero cache counts', cacheRead: 0, cacheWrite: 0 }, + { name: 'omits cache attributes when details are absent', cacheRead: undefined, cacheWrite: undefined }, + ])('$name', async ({ cacheRead, cacheWrite }) => { + const attributes: Record = {}; + const span = { + isRecording: () => true, + setAttributes: (values: Record) => Object.assign(attributes, values), + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + end: () => {}, + } as unknown as Span; + const details = + cacheRead === undefined ? undefined : { cached_tokens: cacheRead, cache_write_tokens: cacheWrite }; + const openAiResponse = { + id: 'response_cache_usage', + model: 'gpt-4o', + choices: [], + usage: + provider === 'openai chat' + ? { prompt_tokens: 2600, completion_tokens: 120, total_tokens: 2720, prompt_tokens_details: details } + : { input_tokens: 2600, output_tokens: 120, total_tokens: 2720, input_tokens_details: details }, + }; + const googleResponse = { + usageMetadata: { + promptTokenCount: 2600, + candidatesTokenCount: 120, + totalTokenCount: 2720, + cachedContentTokenCount: cacheRead, + }, + }; + + if (mode === 'response') { + if (provider === 'google') { + addGoogleResponseAttributes(span, googleResponse, false); + } else { + addOpenAiResponseAttributes(span, openAiResponse, false); + } + } else if (provider === 'google') { + const stream = (async function* () { + yield googleResponse; + yield googleResponse; + yield {}; + })(); + for await (const _ of instrumentGoogleStream(stream, span, false)) { + void _; + } + } else { + const event = + provider === 'openai chat' + ? { ...openAiResponse, object: 'chat.completion.chunk' } + : { type: 'response.completed', response: openAiResponse }; + const responseWithoutCacheDetails = { + ...openAiResponse, + usage: { ...openAiResponse.usage, prompt_tokens_details: undefined, input_tokens_details: undefined }, + }; + const stream = (async function* () { + yield event; + yield event; + yield provider === 'openai chat' + ? { ...responseWithoutCacheDetails, object: 'chat.completion.chunk' } + : { type: 'response.completed', response: responseWithoutCacheDetails }; + })(); + for await (const _ of instrumentOpenAiStream(stream, span, false)) { + void _; + } + } + + expect(attributes).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + ...(cacheRead === undefined ? {} : { [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: cacheRead }), + ...(provider === 'google' || cacheWrite === undefined + ? {} + : { [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: cacheWrite }), + ...(provider === 'google' + ? {} + : { [GEN_AI_RESPONSE_ID]: 'response_cache_usage', [GEN_AI_RESPONSE_MODEL]: 'gpt-4o' }), + ...(mode === 'response' ? {} : { [GEN_AI_RESPONSE_STREAMING]: true }), + }); + }); + }); +}); + +describe.each(['response', 'stream'] as const)('Google reasoning usage (%s)', mode => { + it.each([ + { + name: 'includes reasoning in output', + candidates: 120, + thoughts: 80, + total: 2800, + output: 200, + expectedTotal: 2800, + }, + { + name: 'computes a total including reasoning', + candidates: 120, + thoughts: 80, + total: undefined, + output: 200, + expectedTotal: 2800, + }, + { + name: 'preserves zero reasoning usage', + candidates: 120, + thoughts: 0, + total: 2720, + output: 120, + expectedTotal: 2720, + }, + { + name: 'records reasoning without response candidates', + candidates: undefined, + thoughts: 80, + total: 2680, + output: 80, + expectedTotal: 2680, + }, + ])('$name', async ({ candidates, thoughts, total, output, expectedTotal }) => { + const attributes: Record = {}; + const span = { + isRecording: () => true, + setAttributes: (values: Record) => Object.assign(attributes, values), + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + end: () => {}, + } as unknown as Span; + const response = { + usageMetadata: { + promptTokenCount: 2600, + cachedContentTokenCount: 2048, + candidatesTokenCount: candidates, + thoughtsTokenCount: thoughts, + totalTokenCount: total, + }, + }; + + if (mode === 'response') { + addGoogleResponseAttributes(span, response, false); + } else { + const stream = (async function* () { + yield { usageMetadata: { candidatesTokenCount: candidates } }; + yield { usageMetadata: { thoughtsTokenCount: thoughts } }; + yield response; + yield response; + yield {}; + })(); + for await (const _ of instrumentGoogleStream(stream, span, false)) { + void _; + } + } + + expect(attributes).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048, + [GEN_AI_USAGE_OUTPUT_TOKENS]: output, + [GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]: thoughts, + [GEN_AI_USAGE_TOTAL_TOKENS]: expectedTotal, + ...(mode === 'stream' ? { [GEN_AI_RESPONSE_STREAMING]: true } : {}), + }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts b/packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts new file mode 100644 index 000000000000..a9c7d9da2295 --- /dev/null +++ b/packages/server-utils/test/ai/lib/tracing/langchain-token-usage.test.ts @@ -0,0 +1,264 @@ +import { + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; +import { describe, expect, it } from 'vitest'; +import { extractLlmResponseAttributes } from '../../../../src/ai/langchain/utils'; +import type { LangChainLLMResult } from '../../../../src/ai/langchain/types'; + +const rawUsage = { + input_tokens: 40, + output_tokens: 120, + cache_read_input_tokens: 2048, + cache_creation_input_tokens: 512, +}; +const normalizedUsage = { + input_tokens: 2600, + output_tokens: 120, + total_tokens: 2720, + input_token_details: { cache_read: 2048, cache_creation: 512 }, +}; +const expectedUsage = { + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 2048, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 512, +}; + +describe('LangChain token usage', () => { + it('normalizes raw Anthropic input usage including cache reads and writes', () => { + const result = { generations: [], llmOutput: { usage: rawUsage } }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('does not add raw usage onto normalized message usage', () => { + const result = { + generations: [[{ message: { usage_metadata: normalizedUsage } }]], + llmOutput: { usage: rawUsage }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('records normalized message usage when a streamed result has no llmOutput', () => { + const result = { generations: [[{ message: { usage_metadata: normalizedUsage } }]] }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('preserves normalized totals when tokenUsage contains only the final stream chunk', () => { + const result = { + generations: [[{ message: { usage_metadata: normalizedUsage } }]], + llmOutput: { tokenUsage: { promptTokens: 0, completionTokens: 120, totalTokens: 120 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('prefers normalized message usage when both raw and generic usage are present', () => { + const result = { + generations: [[{ message: { usage_metadata: normalizedUsage } }]], + llmOutput: { + usage: rawUsage, + tokenUsage: { promptTokens: 40, completionTokens: 120, totalTokens: 160 }, + }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('normalizes raw Anthropic usage instead of using exclusive generic totals', () => { + const result = { + generations: [], + llmOutput: { + usage: rawUsage, + tokenUsage: { promptTokens: 40, completionTokens: 120, totalTokens: 160 }, + }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it.each([ + { name: 'inclusive totals', promptTokens: 2600, completionTokens: 120, totalTokens: 2720 }, + { name: 'zero counts', promptTokens: 0, completionTokens: 0, totalTokens: 0 }, + ])('preserves legacy message $name over raw Anthropic usage', ({ promptTokens, completionTokens, totalTokens }) => { + const result = { + generations: [ + [ + { + message: { + response_metadata: { tokenUsage: { promptTokens, completionTokens, totalTokens } }, + }, + }, + ], + ], + llmOutput: { usage: { ...rawUsage, input_tokens: 0, output_tokens: 5 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + ...expectedUsage, + [GEN_AI_USAGE_INPUT_TOKENS]: promptTokens, + [GEN_AI_USAGE_OUTPUT_TOKENS]: completionTokens, + [GEN_AI_USAGE_TOTAL_TOKENS]: totalTokens, + }); + }); + + it('preserves raw cache details when normalized usage reports only inclusive totals', () => { + const result = { + generations: [[{ message: { usage_metadata: { input_tokens: 2600, output_tokens: 120, total_tokens: 2720 } } }]], + llmOutput: { usage: rawUsage }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual(expectedUsage); + }); + + it('aggregates normalized usage across batched prompts', () => { + const result = { + generations: [ + [{ message: { usage_metadata: normalizedUsage } }], + [{ message: { usage_metadata: normalizedUsage } }], + ], + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 5200, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 240, + [GEN_AI_USAGE_TOTAL_TOKENS]: 5440, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 4096, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 1024, + }); + }); + + it('counts shared usage totals once for multiple candidates', () => { + const aggregateUsage = { ...normalizedUsage, output_tokens: 240, total_tokens: 2840 }; + const result = { + generations: [[{ message: { usage_metadata: aggregateUsage } }, { message: { usage_metadata: aggregateUsage } }]], + llmOutput: { tokenUsage: { promptTokens: 2600, completionTokens: 240, totalTokens: 2840 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + ...expectedUsage, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 240, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2840, + }); + }); + + it.each([ + { + name: 'raw Anthropic usage', + result: { + generations: [], + llmOutput: { + usage: { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + }, + }, + }, + { + name: 'normalized usage', + result: { + generations: [ + [ + { + message: { + usage_metadata: { + input_tokens: 0, + output_tokens: 0, + total_tokens: 0, + input_token_details: { cache_read: 0, cache_creation: 0 }, + }, + }, + }, + ], + ], + }, + }, + ])('preserves zero counts in $name', ({ result }) => { + expect(extractLlmResponseAttributes(result as LangChainLLMResult, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 0, + [GEN_AI_USAGE_TOTAL_TOKENS]: 0, + [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: 0, + }); + }); + + it('does not invent cache counts for legacy OpenAI usage', () => { + const result = { + generations: [], + llmOutput: { tokenUsage: { promptTokens: 2600, completionTokens: 120, totalTokens: 2720 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + }); + }); + + it('preserves legacy message totals over final stream chunk counters', () => { + const result = { + generations: [ + [ + { + message: { + response_metadata: { tokenUsage: { promptTokens: 2600, completionTokens: 120, totalTokens: 2720 } }, + }, + }, + ], + ], + llmOutput: { tokenUsage: { promptTokens: 0, completionTokens: 120, totalTokens: 120 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 2600, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 2720, + }); + }); + + it('preserves zero legacy message usage over generic counters', () => { + const result = { + generations: [ + [{ message: { response_metadata: { tokenUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 } } } }], + ], + llmOutput: { tokenUsage: { promptTokens: 2600, completionTokens: 120, totalTokens: 2720 } }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 0, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 0, + [GEN_AI_USAGE_TOTAL_TOKENS]: 0, + }); + }); + + it('omits null cache counts in raw Anthropic usage', () => { + const result = { + generations: [], + llmOutput: { + usage: { + input_tokens: 40, + output_tokens: 120, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + }, + }, + }; + + expect(extractLlmResponseAttributes(result, false)).toEqual({ + [GEN_AI_USAGE_INPUT_TOKENS]: 40, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 120, + [GEN_AI_USAGE_TOTAL_TOKENS]: 160, + }); + }); +}); diff --git a/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts b/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts index cba539ae6e3a..a14324871f8a 100644 --- a/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts +++ b/packages/server-utils/test/ai/lib/utils/langgraph-utils.test.ts @@ -1,5 +1,69 @@ +import { + GEN_AI_RESPONSE_TEXT, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_USAGE_TOTAL_TOKENS, +} from '@sentry/conventions/attributes'; import { describe, expect, it } from 'vitest'; -import { extractAgentNameFromParams, extractLLMFromParams } from '../../../../src/ai/langgraph/utils'; +import type { Span } from '@sentry/core'; +import { + extractAgentNameFromParams, + extractLLMFromParams, + setResponseAttributes, +} from '../../../../src/ai/langgraph/utils'; + +describe('LangGraph cache usage', () => { + it.each([ + { name: 'positive counts', cacheRead: 2048, cacheWrite: 512 }, + { name: 'zero counts', cacheRead: 0, cacheWrite: 0 }, + { name: 'absent counts', cacheRead: undefined, cacheWrite: undefined }, + ])('aggregates $name across new messages without adding caches to input totals', ({ cacheRead, cacheWrite }) => { + const attributes: Record = {}; + const span = { + setAttribute: (key: string, value: unknown) => { + attributes[key] = value; + }, + } as unknown as Span; + const inputMessages = [ + { + role: 'assistant', + content: 'Previous answer', + usage_metadata: { + input_tokens: 10000, + output_tokens: 100, + total_tokens: 10100, + input_token_details: { cache_read: 9000, cache_creation: 1000 }, + }, + }, + ]; + const usage = { + input_tokens: 2600, + output_tokens: 120, + total_tokens: 2720, + input_token_details: { cache_read: cacheRead, cache_creation: cacheWrite }, + }; + + setResponseAttributes(span, inputMessages, { + messages: [ + ...inputMessages, + { role: 'assistant', content: 'First answer', usage_metadata: usage }, + { role: 'assistant', content: 'Second answer', usage_metadata: usage }, + ], + }); + + expect(attributes).toEqual({ + [GEN_AI_RESPONSE_TEXT]: + '[{"role":"assistant","content":"First answer"},{"role":"assistant","content":"Second answer"}]', + [GEN_AI_USAGE_INPUT_TOKENS]: 5200, + [GEN_AI_USAGE_OUTPUT_TOKENS]: 240, + [GEN_AI_USAGE_TOTAL_TOKENS]: 5440, + ...(cacheRead === undefined ? {} : { [GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: cacheRead * 2 }), + ...(cacheWrite === undefined ? {} : { [GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: cacheWrite * 2 }), + }); + }); +}); describe('extractLLMFromParams', () => { it('returns null for empty or invalid args', () => { From b47fd85d853b80fe6184490534d559f633cae8cf Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 7 Oct 2026 11:17:23 +0200 Subject: [PATCH 73/73] meta(changelog): Update changelog for 11.5.0 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b14891dde78..8ca19496299f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,94 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 11.5.0 + +### Important Changes + +- **feat(core): Warn on repeated `Sentry.init()` and unbind the client on `close()` ([#24962](https://github.com/getsentry/sentry-javascript/pull/24962))** + + `Sentry.init()` now warns when it runs while a client is still active. For now, the new client still replaces the active client, but the active client is not closed, so state from both can mix. Call `Sentry.init()` once, or call `await Sentry.close()` before you call it again. `Sentry.close()` now unbinds the client it closes, so after `close()`, `getClient()` returns `undefined`, `isInitialized()` returns `false`, and a later `init()` sets up a new client. In `@sentry/cloudflare`, later requests no longer reuse the isolate's cached client once it is closed or closing. On the server, `@sentry/nextjs` and `@sentry/remix` now return the active client from a repeated `init()` call, not `undefined`. + +### Other Changes + +- chore(deps): Bump rrweb to 2.44.1 ([#25100](https://github.com/getsentry/sentry-javascript/pull/25100)) +- chore(deps): Bump web-vitals to 6.2.3 ([#25062](https://github.com/getsentry/sentry-javascript/pull/25062)) +- feat(bun): keep server spans open until streaming responses finish ([#25025](https://github.com/getsentry/sentry-javascript/pull/25025)) +- feat(langchain): Derive gen_ai.conversation.id from the invoke config ([#24831](https://github.com/getsentry/sentry-javascript/pull/24831)) +- feat(nextjs): Add `code.file.path` to `use cache` fill spans ([#24988](https://github.com/getsentry/sentry-javascript/pull/24988)) +- feat(react-router): Export createSentryServerInstrumentation from the Cloudflare entry ([#25042](https://github.com/getsentry/sentry-javascript/pull/25042)) +- feat(remix): Parameterize Remix 3 navigation spans ([#25045](https://github.com/getsentry/sentry-javascript/pull/25045)) +- feat(remix): Parameterize Remix 3 page load spans ([#25053](https://github.com/getsentry/sentry-javascript/pull/25053)) +- feat(remix): Report the matched route on Remix 3 HTML responses ([#25052](https://github.com/getsentry/sentry-javascript/pull/25052)) +- fix(ai): Normalize token usage and preserve cache breakdowns ([#25074](https://github.com/getsentry/sentry-javascript/pull/25074)) +- fix(browser-utils): Handle NS_ERROR_NOT_INITIALIZED from setTimeout in Firefox ([#25024](https://github.com/getsentry/sentry-javascript/pull/25024)) +- fix(bun): Add conversationIdIntegration to the default integrations ([#25003](https://github.com/getsentry/sentry-javascript/pull/25003)) +- fix(cloudflare): Detect TypeSafe Jev calls in Workers AI by model ID ([#25033](https://github.com/getsentry/sentry-javascript/pull/25033)) +- fix(cloudflare): Keep an installed OpenTelemetry async context strategy ([#24995](https://github.com/getsentry/sentry-javascript/pull/24995)) +- fix(feedback): Correct overlapping screenshot annotations and drag dimming ([#25040](https://github.com/getsentry/sentry-javascript/pull/25040)) +- fix(nextjs): Don't capture forbidden() and unauthorized() as errors ([#25088](https://github.com/getsentry/sentry-javascript/pull/25088)) +- fix(nextjs): Include basePath in request url of Pages Router errors ([#24985](https://github.com/getsentry/sentry-javascript/pull/24985)) +- fix(nextjs): Skip `init` inside a request of `@sentry/cloudflare` ([#24996](https://github.com/getsentry/sentry-javascript/pull/24996)) +- fix(node): Flush telemetry before Vercel functions are suspended ([#24910](https://github.com/getsentry/sentry-javascript/pull/24910)) +- fix(nuxt): Do not inject debug IDs into the SSR build ([#25080](https://github.com/getsentry/sentry-javascript/pull/25080)) +- fix(react-router): Pass the project when creating a release ([#25089](https://github.com/getsentry/sentry-javascript/pull/25089)) +- fix(remix): Pass the project when creating a release ([#25079](https://github.com/getsentry/sentry-javascript/pull/25079)) +- fix(server-utils): Derive the Vercel AI conversation id from the OpenAI conversation option ([#24979](https://github.com/getsentry/sentry-javascript/pull/24979)) +- fix(server-utils): Instrument the Vercel AI experimental_decide channel event ([#25068](https://github.com/getsentry/sentry-javascript/pull/25068)) +- fix(server-utils): Map Flue messages and token usage to the gen_ai conventions ([#24992](https://github.com/getsentry/sentry-javascript/pull/24992)) +- fix(server-utils): Record Vercel AI `experimental_telemetry.metadata` on gen_ai spans ([#24721](https://github.com/getsentry/sentry-javascript/pull/24721)) +- fix(sveltekit): Export consoleLoggingIntegration from worker entry ([#25061](https://github.com/getsentry/sentry-javascript/pull/25061)) +- fix(tanstackstart-react): Fix server route parametrization gaps ([#25071](https://github.com/getsentry/sentry-javascript/pull/25071)) +- ref(core): Deprecate SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ([#25039](https://github.com/getsentry/sentry-javascript/pull/25039)) +- ref(core): Remove `beforeSendSpan` null return warning ([#24129](https://github.com/getsentry/sentry-javascript/pull/24129)) + +
+ Internal Changes + +- chore: Add external contributor to CHANGELOG.md ([#25111](https://github.com/getsentry/sentry-javascript/pull/25111)) +- chore(bugbot): Flag hard-coded attribute names that have a conventions constant ([#25070](https://github.com/getsentry/sentry-javascript/pull/25070)) +- chore(deps-dev): bump nx from 22.7.7 to 22.7.10 ([#25075](https://github.com/getsentry/sentry-javascript/pull/25075)) +- chore(deps): Bump `@sentry/conventions` to 0.26.0 ([#25073](https://github.com/getsentry/sentry-javascript/pull/25073)) +- chore(github): Remove triage-issue workflow ([#25098](https://github.com/getsentry/sentry-javascript/pull/25098)) +- chore(skills): Drop bundler-plugins cross-repo search from triage-issue skill ([#25099](https://github.com/getsentry/sentry-javascript/pull/25099)) +- feat(deps): bump compression from 1.8.1 to 1.8.2 ([#25109](https://github.com/getsentry/sentry-javascript/pull/25109)) +- feat(deps): Bump js-yaml from 3.15.1 to 3.15.2 ([#24236](https://github.com/getsentry/sentry-javascript/pull/24236)) +- feat(deps): bump proxy-addr from 2.0.7 to 2.0.8 ([#25077](https://github.com/getsentry/sentry-javascript/pull/25077)) +- feat(deps): bump shell-quote from 1.10.0 to 1.12.0 ([#25108](https://github.com/getsentry/sentry-javascript/pull/25108)) +- feat(deps): bump source-map-js from 1.2.1 to 1.2.2 ([#25076](https://github.com/getsentry/sentry-javascript/pull/25076)) +- feat(deps): Bump svgo from 4.0.2 to 4.1.0 ([#24220](https://github.com/getsentry/sentry-javascript/pull/24220)) +- fix(deps): Bump seroval to 1.6.8 to fix two Dependabot alerts ([#25104](https://github.com/getsentry/sentry-javascript/pull/25104)) +- ref: Reuse `isObjectLike` for object guards ([#25058](https://github.com/getsentry/sentry-javascript/pull/25058)) +- ref(browser): Use SENTRY_ORIGIN convention constant ([#25037](https://github.com/getsentry/sentry-javascript/pull/25037)) +- ref(core): Use SENTRY_OP convention constant ([#24982](https://github.com/getsentry/sentry-javascript/pull/24982)) +- ref(core): Use SENTRY_ORIGIN convention constant in shared tracing ([#24983](https://github.com/getsentry/sentry-javascript/pull/24983)) +- ref(meta-frameworks): Use SENTRY_ORIGIN convention constant ([#25038](https://github.com/getsentry/sentry-javascript/pull/25038)) +- ref(nextjs): Move the server span hooks into `serverSpanHooks.ts` ([#24997](https://github.com/getsentry/sentry-javascript/pull/24997)) +- ref(server): Use SENTRY_ORIGIN convention constant ([#25036](https://github.com/getsentry/sentry-javascript/pull/25036)) +- test(aws-serverless): Port GraphQL test to span streaming ([#25056](https://github.com/getsentry/sentry-javascript/pull/25056)) +- test(browser): port general browser integration suites to default streaming ([#24876](https://github.com/getsentry/sentry-javascript/pull/24876)) +- test(bun): Port request-body tests to span streaming ([#25021](https://github.com/getsentry/sentry-javascript/pull/25021)) +- test(cloudflare): Add Workers AI tracing tests for Clef ([#25027](https://github.com/getsentry/sentry-javascript/pull/25027)) +- test(e2e): Add Bun, Deno and Cloudflare variants to react-router-8-framework ([#24598](https://github.com/getsentry/sentry-javascript/pull/24598)) +- test(e2e): Add eve test for Jev evaluate calls ([#25034](https://github.com/getsentry/sentry-javascript/pull/25034)) +- test(e2e): Bump bundler plugins to 5.4.1 in e2e test apps ([#25107](https://github.com/getsentry/sentry-javascript/pull/25107)) +- test(e2e): Cover isolation, trace propagation and logs on all react-router-8-framework runtimes ([#25041](https://github.com/getsentry/sentry-javascript/pull/25041)) +- test(e2e): Move react-router-8-cloudflare MySQL tests into react-router-8-framework ([#24599](https://github.com/getsentry/sentry-javascript/pull/24599)) +- test(e2e): Port Astro 5 Cloudflare to span streaming ([#25016](https://github.com/getsentry/sentry-javascript/pull/25016)) +- test(e2e): Raise mock Sentry server chunk size to fit whole bundles ([#25097](https://github.com/getsentry/sentry-javascript/pull/25097)) +- test(ember): Pin embroider packages away from broken releases ([#25066](https://github.com/getsentry/sentry-javascript/pull/25066)) +- test(nextjs): Expect use cache route handler file path on Turbopack ([#25117](https://github.com/getsentry/sentry-javascript/pull/25117)) +- test(nextjs): Pin nextjs-16-cf-workers latest variant to Next.js 16.3 ([#25118](https://github.com/getsentry/sentry-javascript/pull/25118)) +- test(node): Port Apollo GraphQL integration tests to span streaming ([#25030](https://github.com/getsentry/sentry-javascript/pull/25030)) +- test(node): port AWS integration tests to span streaming ([#25018](https://github.com/getsentry/sentry-javascript/pull/25018)) +- test(node): port filesystem integration tests to span streaming ([#25019](https://github.com/getsentry/sentry-javascript/pull/25019)) +- test(node): Port GraphQL tracing-channel tests to span streaming ([#25055](https://github.com/getsentry/sentry-javascript/pull/25055)) +- test(node): port rejection, feature flag, and client-report tests to span streaming ([#25020](https://github.com/getsentry/sentry-javascript/pull/25020)) +- test(node/bun): drop static pins from non-tracing integration suites ([#25017](https://github.com/getsentry/sentry-javascript/pull/25017)) +- test(nuxt): Upgrade to Nuxt 4.6 ; Test imports from `nuxt/server` ([#25093](https://github.com/getsentry/sentry-javascript/pull/25093)) + +
+ Work in this release was contributed by @minwookshin. Thank you for your contribution! ## 11.4.0 @@ -92,8 +180,6 @@ Work in this release was contributed by @minwookshin. Thank you for your contrib Work in this release was contributed by @Shubham-Padkonde and @tobias-schnabel. Thank you for your contributions! -- **feat(core)**: `Sentry.init()` now warns when it runs while a client is still active. For now, the new client still replaces the active client, but the active client is not closed, so state from both can mix. Call `Sentry.init()` once, or call `await Sentry.close()` before you call it again. `Sentry.close()` now unbinds the client it closes, so after `close()`, `getClient()` returns `undefined`, `isInitialized()` returns `false`, and a later `init()` sets up a new client. In `@sentry/cloudflare`, later requests no longer reuse the isolate's cached client once it is closed or closing. On the server, `@sentry/nextjs` and `@sentry/remix` now return the active client from a repeated `init()` call, not `undefined`. - ## 11.2.0 ### Important Changes