diff --git a/.github/workflows/auto-fix-issue.yml b/.github/workflows/auto-fix-issue.yml
index 245ffc18b3f4..559f255e3e98 100644
--- a/.github/workflows/auto-fix-issue.yml
+++ b/.github/workflows/auto-fix-issue.yml
@@ -77,7 +77,7 @@ jobs:
- name: Try to fix the issue with Claude
id: triage
- uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
+ uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1
env:
ANTHROPIC_BASE_URL: https://openrouter.ai/api
with:
diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml
index d6c654de029a..8a2e73d10327 100644
--- a/.github/workflows/auto-release.yml
+++ b/.github/workflows/auto-release.yml
@@ -51,7 +51,7 @@ jobs:
node-version-file: 'package.json'
- name: Prepare release
- uses: getsentry/craft@cd1e8294061fd970b40d98b77aaa109cb1e00e78 # v2.30.1
+ uses: getsentry/craft@25028d0646040cc0a669ae3314cbf884f4c4347a # v2.31.2
if:
github.event.pull_request.merged == true && steps.version-regex.outputs.match != '' &&
steps.get_version.outputs.version != ''
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 13bfa91520fc..f680c6c3c630 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -886,7 +886,7 @@ jobs:
with:
ref: ${{ env.HEAD_COMMIT }}
# The fixtures install the locally-packed `@sentry/bundler-plugins` tarball with pnpm.
- - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
+ - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 9.15.9
- name: Set up Node
@@ -1046,7 +1046,7 @@ jobs:
uses: actions/checkout@v7
with:
ref: ${{ env.HEAD_COMMIT }}
- - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
+ - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: ${{ matrix.test-application == 'nuxt-5' && '11.10.0' || '9.15.9' }}
- name: Set up Node
@@ -1186,7 +1186,7 @@ jobs:
uses: actions/checkout@v7
with:
ref: ${{ env.HEAD_COMMIT }}
- - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
+ - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: ${{ matrix.test-application == 'nuxt-5' && '11.10.0' || '9.15.9' }}
- name: Set up Node
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index a24354e3497b..fd9c8d6ce524 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -117,7 +117,7 @@ jobs:
uses: actions/checkout@v7
with:
ref: ${{ env.HEAD_COMMIT }}
- - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
+ - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 9.15.9
- name: Set up Node
diff --git a/.github/workflows/cleanup-e2e-workers.yml b/.github/workflows/cleanup-e2e-workers.yml
index f87a387d281b..69aae225e408 100644
--- a/.github/workflows/cleanup-e2e-workers.yml
+++ b/.github/workflows/cleanup-e2e-workers.yml
@@ -19,6 +19,7 @@ jobs:
# Name prefix of every E2E app that deploys a real worker, see the app's global-setup.ts
worker-prefix:
- e2e-send-to-sentry
+ - e2e-flue-send-to-sentry
steps:
- name: Set up Node
uses: actions/setup-node@v7
diff --git a/.github/workflows/dependabot-auto-triage.yml b/.github/workflows/dependabot-auto-triage.yml
index 2d10df4fb2be..3a94bd400aa9 100644
--- a/.github/workflows/dependabot-auto-triage.yml
+++ b/.github/workflows/dependabot-auto-triage.yml
@@ -160,7 +160,7 @@ jobs:
git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"
- name: Open batched runtime fix PR via skill
- uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
+ uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1
env:
ANTHROPIC_BASE_URL: https://openrouter.ai/api
with:
@@ -249,7 +249,7 @@ jobs:
git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com"
- name: Open batched dev fix PR via skill
- uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
+ uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1
env:
ANTHROPIC_BASE_URL: https://openrouter.ai/api
with:
diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml
index 6df0c712f2bd..6928d5d4073e 100644
--- a/.github/workflows/lighthouse.yml
+++ b/.github/workflows/lighthouse.yml
@@ -19,7 +19,7 @@ jobs:
VITE_E2E_TEST_DSN: 'https://username@domain/123'
steps:
- uses: actions/checkout@v7
- - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
+ - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 9.15.9
- uses: actions/setup-node@v7
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0c3d5d08c93c..7840b35f677a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,7 +36,7 @@ jobs:
with:
node-version-file: 'package.json'
- name: Prepare release
- uses: getsentry/craft@cd1e8294061fd970b40d98b77aaa109cb1e00e78 # v2.30.1
+ uses: getsentry/craft@25028d0646040cc0a669ae3314cbf884f4c4347a # v2.31.2
env:
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
with:
diff --git a/.github/workflows/track-framework-updates.yml b/.github/workflows/track-framework-updates.yml
index 723d1661c0d2..0a74bad0b923 100644
--- a/.github/workflows/track-framework-updates.yml
+++ b/.github/workflows/track-framework-updates.yml
@@ -43,7 +43,7 @@ jobs:
- name: Run Claude digest
id: digest
- uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
+ uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1
env:
ANTHROPIC_BASE_URL: https://openrouter.ai/api
with:
diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml
index 9994c56b9900..78ec8b6586e5 100644
--- a/.github/workflows/triage-issue.yml
+++ b/.github/workflows/triage-issue.yml
@@ -54,7 +54,7 @@ jobs:
- name: Run Claude triage
id: triage
- uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
+ uses: anthropics/claude-code-action@8ce9314fa9a404564fa7e954cd84f25bcba2b829 # v1
env:
ANTHROPIC_BASE_URL: https://openrouter.ai/api
with:
diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml
index 7ccbd41bf16d..9e29641264dc 100644
--- a/.github/workflows/validate-pr.yml
+++ b/.github/workflows/validate-pr.yml
@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- - uses: getsentry/github-workflows/validate-pr@4013fc6e1aeb1be1f9d3b4d232624f0ec1afa613
+ - uses: getsentry/github-workflows/validate-pr@36c729264d2edc29ebae61950c50e1e9f043ad7e
with:
app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }}
private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }}
diff --git a/.size-limit.js b/.size-limit.js
index 80ad8c9f5f4f..220fa9c349e8 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -89,7 +89,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
- limit: '96 KB',
+ limit: '97 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -97,7 +97,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
- limit: '85 KB',
+ limit: '86 KB',
disablePlugins: ['@size-limit/esbuild'],
modifyWebpackConfig: function (config) {
const webpack = require('webpack');
@@ -137,7 +137,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'feedbackIntegration'),
gzip: true,
- limit: '52 KB',
+ limit: '53 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -187,7 +187,7 @@ module.exports = [
import: createImport('init', 'ErrorBoundary'),
ignore: ['react/jsx-runtime'],
gzip: true,
- limit: '36 KB',
+ limit: '37 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -205,7 +205,7 @@ module.exports = [
path: 'packages/vue/build/esm/index.js',
import: createImport('init'),
gzip: true,
- limit: '42 KB',
+ limit: '43 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -213,7 +213,7 @@ module.exports = [
path: 'packages/vue/build/esm/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
- limit: '59 KB',
+ limit: '60 KB',
disablePlugins: ['@size-limit/esbuild'],
},
// Svelte SDK (ESM)
@@ -225,12 +225,25 @@ module.exports = [
limit: '35 KB',
disablePlugins: ['@size-limit/esbuild'],
},
+ // Remix 3 browser SDK (ESM)
+ {
+ // Every export, not a named import: Remix 3 has no bundler, so an app ships every byte of this
+ // file. The budget is what keeps `src/v3/index.client.ts` a named list. One added
+ // `export * from '@sentry/browser'` measures 134 KB here, and more on the wire, because a real app
+ // has no bundler to shake it.
+ name: '@sentry/remix (Remix 3 client bundle)',
+ path: 'packages/remix/build/esm/v3/client-bundle.js',
+ import: '*',
+ gzip: true,
+ limit: '61 KB',
+ disablePlugins: ['@size-limit/esbuild'],
+ },
// Browser CDN bundles
{
name: 'CDN Bundle',
path: createCDNPath('bundle.min.js'),
gzip: true,
- limit: '36 KB',
+ limit: '37 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -258,7 +271,7 @@ module.exports = [
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
gzip: true,
- limit: '79 KB',
+ limit: '80 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -295,7 +308,7 @@ module.exports = [
path: createCDNPath('bundle.min.js'),
gzip: false,
brotli: false,
- limit: '97 KB',
+ limit: '98 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -303,7 +316,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
- limit: '159 KB',
+ limit: '160 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -311,7 +324,7 @@ module.exports = [
path: createCDNPath('bundle.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '103 KB',
+ limit: '104 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -319,7 +332,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '165 KB',
+ limit: '166 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -327,7 +340,7 @@ module.exports = [
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '233 KB',
+ limit: '234 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -335,7 +348,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: false,
brotli: false,
- limit: '279 KB',
+ limit: '280 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -343,7 +356,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
- limit: '285 KB',
+ limit: '286 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -369,7 +382,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
- limit: '61 KB',
+ limit: '62 KB',
disablePlugins: ['@size-limit/esbuild'],
},
// SvelteKit SDK (ESM)
@@ -388,7 +401,7 @@ module.exports = [
path: 'packages/core/build/esm/server.js',
import: '*',
gzip: true,
- limit: '45 KB',
+ limit: '46 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -406,7 +419,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '145 KB',
+ limit: '150 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
@@ -414,7 +427,7 @@ module.exports = [
path: ['packages/server-runtime-injection/build/esm/hook.js', 'packages/node/build/import-hook.mjs'],
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '88 KB',
+ limit: '89 KB',
disablePlugins: ['@size-limit/esbuild'],
modifyWebpackConfig: function (config) {
// Both packages declare `sideEffects: false`, which lets webpack
@@ -430,7 +443,7 @@ module.exports = [
path: 'packages/node/build/esm/index.js',
import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'),
gzip: true,
- limit: '96 KB',
+ limit: '99 KB',
disablePlugins: ['@size-limit/esbuild'],
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
modifyWebpackConfig: function (config) {
@@ -452,7 +465,7 @@ module.exports = [
path: 'packages/node/build/esm/index.js',
import: createImport('init'),
gzip: true,
- limit: '123 KB',
+ limit: '128 KB',
disablePlugins: ['@size-limit/esbuild'],
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
modifyWebpackConfig: function (config) {
@@ -473,7 +486,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '104 KB',
+ limit: '107 KB',
disablePlugins: ['@size-limit/esbuild'],
},
// Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output
@@ -484,7 +497,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
- limit: '208 KiB',
+ limit: '209 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
@@ -504,7 +517,7 @@ module.exports = [
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: false,
brotli: false,
- limit: '507 KiB',
+ limit: '512 KiB',
disablePlugins: ['@size-limit/webpack'],
webpack: false,
modifyEsbuildConfig: function (config) {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec252e25122e..8f710752af09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,87 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 11.3.0
+
+### Important Changes
+
+- **Remix 3 support (alpha, may break in minor releases)**
+
+ - feat(remix): Add Remix 3 browser SDK with a bundled client entry ([#24802](https://github.com/getsentry/sentry-javascript/pull/24802))
+ - feat(remix): Add source map upload for Remix 3 ([#24943](https://github.com/getsentry/sentry-javascript/pull/24943))
+ - feat(remix): Capture Remix 3 component render errors ([#24873](https://github.com/getsentry/sentry-javascript/pull/24873))
+ - feat(remix): Complete Remix 3 server error handling ([#24878](https://github.com/getsentry/sentry-javascript/pull/24878))
+ - feat(remix): Inject debug IDs through the Remix 3 asset server ([#24761](https://github.com/getsentry/sentry-javascript/pull/24761))
+ - feat(remix): Run the orchestrion transform on Remix 3 browser modules ([#24894](https://github.com/getsentry/sentry-javascript/pull/24894))
+
+- **feat(nextjs): Add `cache_origin` span links to `use cache` hit spans ([#24821](https://github.com/getsentry/sentry-javascript/pull/24821))**
+
+ A `cache.get` span for a `"use cache"` hit now carries a span link (`sentry.link.type: 'cache_origin'`) to the `cache.put` span of the request that filled the cache entry, connecting the trace that reads a cached value to the trace that produced it.
+
+- **fix(cloudflare): Don't treat DO constructor work as incoming RPC calls ([#24829](https://github.com/getsentry/sentry-javascript/pull/24829))**
+
+ Durable Object constructors now run in their own isolation scope. When work that the constructor starts, for example a `blockConcurrencyWhile` callback, calls a method of the instance, that call is no longer treated as an incoming RPC call, so an error the instance catches itself is no longer reported as unhandled. As a result, `Sentry.setTag()`, `setUser()` and `setContext()` in a constructor now apply only to that constructor work. They no longer reach later `fetch`, RPC or `alarm` invocations, because the scope they used to write to is shared by every Durable Object and handler in the isolate. Use `initialScope` for static data, and set per-instance data in each handler.
+
+- **fix(effect)!: Change peerDependency to v4 and fix currentSpan ([#24940](https://github.com/getsentry/sentry-javascript/pull/24940))**
+
+ `@sentry/effect` now requires a stable Effect v4 release. Effect v4 beta and release candidate versions are no longer supported. `@sentry/effect` is in alpha, so this ships in a minor release.
+
+### Other Changes
+
+- feat(astro): Register astro route provider ([#23792](https://github.com/getsentry/sentry-javascript/pull/23792))
+- feat(cloudflare): Trace TypeSafe Jev calls in Workers AI as evaluate spans ([#24833](https://github.com/getsentry/sentry-javascript/pull/24833))
+- feat(core): Add route provider API for parameterized route resolution ([#23551](https://github.com/getsentry/sentry-javascript/pull/23551))
+- feat(deps): bump devalue from 5.9.2 to 5.9.4 ([#24964](https://github.com/getsentry/sentry-javascript/pull/24964))
+- feat(effect): Add opt-in for external span parents and isolate root spans ([#23900](https://github.com/getsentry/sentry-javascript/pull/23900))
+- feat(nextjs): Register nextjs route provider ([#23552](https://github.com/getsentry/sentry-javascript/pull/23552))
+- feat(nuxt): Register nuxt route provider ([#23794](https://github.com/getsentry/sentry-javascript/pull/23794))
+- feat(remix): Register remix route provider ([#23791](https://github.com/getsentry/sentry-javascript/pull/23791))
+- feat(vue): Register Vue route provider ([#23793](https://github.com/getsentry/sentry-javascript/pull/23793))
+- fix(bundler-plugins): Keep debug IDs on Vite source maps after the preload rewrite ([#24920](https://github.com/getsentry/sentry-javascript/pull/24920))
+- fix(bundler-plugins): Stabilize debug IDs for Rolldown and Vite 8 builds ([#24919](https://github.com/getsentry/sentry-javascript/pull/24919))
+- fix(cloudflare): Skip binding instrumentation work when spans are not sent ([#24655](https://github.com/getsentry/sentry-javascript/pull/24655))
+- fix(core, node, bun, deno): Align server span client address with event IP ([#24767](https://github.com/getsentry/sentry-javascript/pull/24767))
+- fix(hono): Return the existing client on repeated init in Bun and Deno ([#24520](https://github.com/getsentry/sentry-javascript/pull/24520))
+- fix(nitro): Capture errors only through the Nitro error hook ([#24952](https://github.com/getsentry/sentry-javascript/pull/24952))
+- fix(profiling-node): Send profile chunks with `client.sendEnvelope` ([#24896](https://github.com/getsentry/sentry-javascript/pull/24896))
+- fix(react-router): Avoid duplicating Vite config arrays ([#24930](https://github.com/getsentry/sentry-javascript/pull/24930))
+- fix(react-router): Resolve the Cloudflare entry in Workers and skip trace meta tags in prerendered pages ([#24866](https://github.com/getsentry/sentry-javascript/pull/24866))
+- fix(server-utils): Match the Anthropic stream helper header regardless of case ([#24855](https://github.com/getsentry/sentry-javascript/pull/24855))
+- fix(server-utils): Skip Fastify 4xx errors raised before the reply status is set ([#24924](https://github.com/getsentry/sentry-javascript/pull/24924))
+- fix(tanstackstart-react): Avoid duplicating Vite config arrays ([#24929](https://github.com/getsentry/sentry-javascript/pull/24929))
+
+
+ Internal Changes
+
+- chore(deps-dev): bump @vercel/nft from 1.5.0 to 1.11.0 ([#24954](https://github.com/getsentry/sentry-javascript/pull/24954))
+- chore(deps): Bump @sentry/conventions to 0.25.0 ([#24958](https://github.com/getsentry/sentry-javascript/pull/24958))
+- chore(deps): dev dependency security fixes ([#24275](https://github.com/getsentry/sentry-javascript/pull/24275))
+- chore(size-limit): weekly auto-bump ([#24969](https://github.com/getsentry/sentry-javascript/pull/24969))
+- ci(deps): bump anthropics/claude-code-action from 1.0.210 to 1.0.236 ([#24950](https://github.com/getsentry/sentry-javascript/pull/24950))
+- ci(deps): bump getsentry/craft from 2.30.1 to 2.31.2 ([#24951](https://github.com/getsentry/sentry-javascript/pull/24951))
+- ci(deps): bump getsentry/github-workflows/validate-pr from 4013fc6e1aeb1be1f9d3b4d232624f0ec1afa613 to 36c729264d2edc29ebae61950c50e1e9f043ad7e ([#24949](https://github.com/getsentry/sentry-javascript/pull/24949))
+- ci(deps): bump pnpm/action-setup from 6.0.10 to 6.1.0 ([#24948](https://github.com/getsentry/sentry-javascript/pull/24948))
+- license: Add cli FSL notice ([#24956](https://github.com/getsentry/sentry-javascript/pull/24956))
+- ref(core): Use cache attribute constants from conventions ([#24973](https://github.com/getsentry/sentry-javascript/pull/24973))
+- ref(core): Use conversation ID constant from conventions ([#24966](https://github.com/getsentry/sentry-javascript/pull/24966))
+- ref(core): Use exclusive time constant from conventions ([#24971](https://github.com/getsentry/sentry-javascript/pull/24971))
+- ref(core): Use HTTP method constant from conventions ([#24977](https://github.com/getsentry/sentry-javascript/pull/24977))
+- ref(core): Use idle span finish reason constant from conventions ([#24970](https://github.com/getsentry/sentry-javascript/pull/24970))
+- ref(core): Use profile ID constant from conventions ([#24976](https://github.com/getsentry/sentry-javascript/pull/24976))
+- ref(core): Use SDK metadata constants from conventions ([#24978](https://github.com/getsentry/sentry-javascript/pull/24978))
+- ref(core): Use status message constant from conventions ([#24967](https://github.com/getsentry/sentry-javascript/pull/24967))
+- ref(core): Use user attribute constants from conventions ([#24974](https://github.com/getsentry/sentry-javascript/pull/24974))
+- ref(server-utils): Clarify API promise helper naming and references ([#24928](https://github.com/getsentry/sentry-javascript/pull/24928))
+- test(cloudflare): Add Flue E2E test that deploys a real Worker and sends to Sentry ([#24816](https://github.com/getsentry/sentry-javascript/pull/24816))
+- test(cloudflare): Match the expected error in the WebSocket e2e tests ([#24923](https://github.com/getsentry/sentry-javascript/pull/24923))
+- test(deno): Port integration tests to span streaming ([#24870](https://github.com/getsentry/sentry-javascript/pull/24870))
+- test(e2e): Add node-anthropic-send-to-sentry test app ([#24856](https://github.com/getsentry/sentry-javascript/pull/24856))
+- test(e2e): Avoid rate limits when polling Sentry in send-to-sentry tests ([#24957](https://github.com/getsentry/sentry-javascript/pull/24957))
+
+
+
+Work in this release was contributed by @Shubham-Padkonde and @tobias-schnabel. Thank you for your contributions!
+
## 11.2.0
### Important Changes
diff --git a/LICENSE b/LICENSE
index 84d8d8c065fc..32c62f9869e3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -19,3 +19,13 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+
+Third-Party Dependency Notice
+
+The MIT License above applies to the code in this repository. Some packages
+published from this repository have the Sentry CLI (the `sentry` package on npm,
+https://github.com/getsentry/cli) as a direct or transitive dependency. The
+Sentry CLI is not part of this repository and is not covered by the MIT License
+above. It is licensed under the Functional Source License, Version 1.1, Apache
+2.0 Future License (FSL-1.1-Apache-2.0):
+https://github.com/getsentry/cli/blob/main/LICENSE.md
diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json
index 30ef30f621b4..b89832568047 100644
--- a/dev-packages/browser-integration-tests/package.json
+++ b/dev-packages/browser-integration-tests/package.json
@@ -63,7 +63,7 @@
"@sentry/browser": "11.2.0",
"@sentry/replay": "11.2.0",
"@sentry/opentelemetry": "11.2.0",
- "@sentry/conventions": "0.24.0",
+ "@sentry/conventions": "0.25.0",
"@supabase/supabase-js": "2.109.0",
"axios": "1.20.0",
"babel-loader": "^10.1.1",
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 62d8bd9c6366..289a5716746f 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,12 +1,9 @@
import { expect } from '@playwright/test';
import {
SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
- SEMANTIC_ATTRIBUTE_SENTRY_STATUS_MESSAGE,
} from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
@@ -19,6 +16,9 @@ import {
SENTRY_SDK_VERSION,
SENTRY_TRACE_LIFECYCLE,
USER_AGENT_ORIGINAL,
+ SENTRY_STATUS_MESSAGE,
+ SENTRY_ENVIRONMENT,
+ SENTRY_SDK_INTEGRATIONS,
} from '@sentry/conventions/attributes';
sentryTest(
@@ -101,7 +101,7 @@ sentryTest(
type: 'string',
value: 'test-span',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
@@ -146,7 +146,7 @@ sentryTest(
type: 'string',
value: 'test-span',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
@@ -191,11 +191,11 @@ sentryTest(
type: 'string',
value: 'test-span',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_STATUS_MESSAGE]: {
+ [SENTRY_STATUS_MESSAGE]: {
type: 'string',
value: 'Connection Refused',
},
@@ -260,7 +260,7 @@ sentryTest(
type: 'string',
value: SDK_VERSION,
},
- [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: {
+ [SENTRY_SDK_INTEGRATIONS]: {
type: 'array',
value: expect.arrayContaining(['SpanStreaming']),
},
@@ -276,7 +276,7 @@ sentryTest(
type: 'string',
value: 'custom',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts
index 70b6f2c66fd4..3ef7ddefca6d 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/default/test.ts
@@ -1,5 +1,6 @@
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { expect } from '@playwright/test';
-import { extractTraceparentData, parseBaggageHeader, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { extractTraceparentData, parseBaggageHeader } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils';
@@ -66,7 +67,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
span_id: customTraceSpan.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts
index 32d62350d6b4..37e3b204fe88 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/consistent-sampling/tracesSampler-precedence/test.ts
@@ -1,7 +1,7 @@
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { expect } from '@playwright/test';
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/browser';
import type { ClientReport } from '@sentry/core';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
@@ -103,7 +103,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
expect(customTrace2Span.links).toEqual([
{
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
@@ -136,7 +136,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
expect(navigationSpan.links).toEqual([
{
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts
index 571f4a6d8b5e..7bb7a5546910 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/custom-trace/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -27,7 +27,7 @@ sentryTest('manually started custom traces are linked correctly in the chain', a
span_id: pageloadSpan.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
@@ -52,7 +52,7 @@ sentryTest('manually started custom traces are linked correctly in the chain', a
span_id: customTraceSpan.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts
index c8d7af8246be..5fb19844a3ce 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/default/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -33,7 +33,7 @@ sentryTest("navigation spans link back to previous trace's root span", async ({
span_id: pageloadSpan.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
@@ -52,7 +52,7 @@ sentryTest("navigation spans link back to previous trace's root span", async ({
span_id: navigation1Span.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts
index 1e5666e116e8..bfc974fc953a 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/interaction-spans/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -52,7 +52,7 @@ sentryTest(
span_id: pageloadSpan.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts
index 141e5a9505dc..732547954091 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/meta/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -37,7 +37,7 @@ sentryTest(
span_id: pageloadSpan.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts
index 8b9b3d044dce..4bc05cb95737 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/negatively-sampled/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -26,7 +26,7 @@ sentryTest('includes a span link to a previously negatively sampled span', async
span_id: expect.stringMatching(/[a-f\d]{16}/),
sampled: false,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts
index 5ed1c8021d4a..81d1d31e10e3 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces-streamed/session-storage/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -30,7 +30,7 @@ sentryTest('adds link between hard page reloads when opting into sessionStorage'
span_id: pageload1Span.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
+ [SENTRY_LINK_TYPE]: {
type: 'string',
value: 'previous_trace',
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts
index 6787b23e6db8..4d13371bc693 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/consistent-sampling/default/test.ts
@@ -1,10 +1,6 @@
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { expect } from '@playwright/test';
-import {
- extractTraceparentData,
- parseBaggageHeader,
- SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE,
-} from '@sentry/core';
+import { extractTraceparentData, parseBaggageHeader, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
eventAndTraceHeaderRequestParser,
@@ -76,7 +72,7 @@ sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
span_id: customTraceContext?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts
index b8d4f43cf03b..c4637dc25018 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/custom-trace/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
@@ -31,7 +31,7 @@ sentryTest('manually started custom traces are linked correctly in the chain', a
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
@@ -54,7 +54,7 @@ sentryTest('manually started custom traces are linked correctly in the chain', a
span_id: customTrace1Context?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts
index 96302d731e5e..7399c2bb26c3 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/default/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
@@ -43,7 +43,7 @@ sentryTest("navigation spans link back to previous trace's root span", async ({
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
@@ -58,7 +58,7 @@ sentryTest("navigation spans link back to previous trace's root span", async ({
span_id: navigation1TraceContext?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts
index f8d01ab4a9d7..24a1e2c59072 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/interaction-spans/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
@@ -61,7 +61,7 @@ sentryTest(
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts
index 123f44e8db85..a232a5a0f6e7 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/meta/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
@@ -44,7 +44,7 @@ sentryTest(
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts
index b84aefb8887d..894fe0d5929a 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/negatively-sampled/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
@@ -28,7 +28,7 @@ sentryTest('includes a span link to a previously negatively sampled span', async
span_id: expect.stringMatching(/[a-f\d]{16}/),
sampled: false,
attributes: {
- [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
+ [SENTRY_LINK_TYPE]: 'previous_trace',
},
},
]);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts
index 19f34eef73f3..f7e0d288de5b 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/linked-traces/session-storage/test.ts
@@ -1,5 +1,5 @@
import { expect } from '@playwright/test';
-import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
+import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
@@ -31,7 +31,7 @@ sentryTest('adds link between hard page reloads when opting into sessionStorage'
trace_id: pageload1TraceContext?.trace_id,
span_id: pageload1TraceContext?.span_id,
sampled: true,
- attributes: { [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace' },
+ attributes: { [SENTRY_LINK_TYPE]: 'previous_trace' },
},
]);
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 b99e676afaae..5d298cb1e9cd 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,11 +1,9 @@
import { expect } from '@playwright/test';
import {
SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
} from '@sentry/core';
import {
SENTRY_SEGMENT_NAME_SOURCE,
@@ -13,6 +11,8 @@ import {
URL_FULL,
URL_PATH,
USER_AGENT_ORIGINAL,
+ SENTRY_ENVIRONMENT,
+ SENTRY_SDK_INTEGRATIONS,
} from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
@@ -148,7 +148,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
type: 'string',
value: SDK_VERSION,
},
- [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: {
+ [SENTRY_SDK_INTEGRATIONS]: {
type: 'array',
value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']),
},
@@ -164,7 +164,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
type: 'string',
value: 'url',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
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 680b868b4659..1fdaaadce2bd 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,11 +1,9 @@
import { expect } from '@playwright/test';
import {
SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
} from '@sentry/core';
import {
BROWSER_NAVIGATION_TYPE,
@@ -18,6 +16,8 @@ import {
URL_FULL,
URL_PATH,
USER_AGENT_ORIGINAL,
+ SENTRY_ENVIRONMENT,
+ SENTRY_SDK_INTEGRATIONS,
} from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
@@ -156,7 +156,7 @@ sentryTest(
type: 'string',
value: SDK_VERSION,
},
- [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: {
+ [SENTRY_SDK_INTEGRATIONS]: {
type: 'array',
value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']),
},
@@ -172,7 +172,7 @@ sentryTest(
type: 'string',
value: 'url',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
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 706166ef0dc4..3d3d4e69069c 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,11 +1,10 @@
-import { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes';
+import { SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_NAME_SOURCE } 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_IDLE_SPAN_FINISH_REASON } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../../utils/spanUtils';
@@ -30,7 +29,7 @@ sentryTest(
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'reportPageLoaded' },
+ [SENTRY_IDLE_SPAN_FINISH_REASON]: { type: 'string', value: 'reportPageLoaded' },
});
// We wait for 2.5 seconds before calling Sentry.reportPageLoaded(). The span starts at navigation start,
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 0f6c7f4d772c..2a6123aa2aa9 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,13 +1,7 @@
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_ENVIRONMENT,
- SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON,
- SEMANTIC_ATTRIBUTE_SENTRY_OP,
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
-} from '@sentry/core';
-import {
+ SENTRY_IDLE_SPAN_FINISH_REASON,
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_SEGMENT_NAME_SOURCE,
@@ -16,6 +10,8 @@ import {
SENTRY_TRACE_LIFECYCLE,
USER_AGENT_ORIGINAL,
URL_PATH,
+ SENTRY_ENVIRONMENT,
+ SENTRY_SDK_INTEGRATIONS,
} from '@sentry/conventions/attributes';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
@@ -76,7 +72,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
type: 'string',
value: '/index.html',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: {
+ [SENTRY_IDLE_SPAN_FINISH_REASON]: {
type: 'string',
value: 'idleTimeout',
},
@@ -96,7 +92,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
type: 'string',
value: SDK_VERSION,
},
- [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: {
+ [SENTRY_SDK_INTEGRATIONS]: {
type: 'array',
value: expect.arrayContaining(['BrowserTracing', 'SpanStreaming']),
},
@@ -112,7 +108,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
type: 'string',
value: 'custom',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
@@ -165,7 +161,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
type: 'string',
value: 'Click',
},
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
+ [SENTRY_ENVIRONMENT]: {
type: 'string',
value: 'production',
},
diff --git a/dev-packages/bun-integration-tests/node-suites/excludes.ts b/dev-packages/bun-integration-tests/node-suites/excludes.ts
index c9a89b180612..20124be5a264 100644
--- a/dev-packages/bun-integration-tests/node-suites/excludes.ts
+++ b/dev-packages/bun-integration-tests/node-suites/excludes.ts
@@ -83,6 +83,7 @@ export const NO_AUTO_INSTRUMENTATION = [
'suites/pino/test.ts',
'suites/tracing/amqplib/test.ts',
'suites/tracing/anthropic/test.ts',
+ 'suites/tracing/anthropic/v0.129/test.ts',
'suites/tracing/apollo-graphql/**',
'suites/tracing/dataloader/test.ts',
'suites/tracing/fastify/test.ts',
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.d.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.d.ts
new file mode 100644
index 000000000000..192b614bc29b
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.d.ts
@@ -0,0 +1,2 @@
+import type { SentryRollupPluginOptions } from "@sentry/bundler-plugins/rollup";
+export declare const sentryConfig: SentryRollupPluginOptions;
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.js
new file mode 100644
index 000000000000..91b4c2a30c83
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/configs/sourcemaps-disable-upload.config.js
@@ -0,0 +1,6 @@
+export const sentryConfig = {
+ telemetry: false,
+ sourcemaps: {
+ disable: "disable-upload",
+ },
+};
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/debugids-already-injected.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/debugids-already-injected.test.ts
index c5a04f975b7c..950873a28a70 100644
--- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/debugids-already-injected.test.ts
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/debugids-already-injected.test.ts
@@ -2,14 +2,24 @@ import { expect } from "vitest";
import { readAllFiles } from "../utils";
import { test } from "./utils";
+const normalizeDebugIds = (value: string): string =>
+ value.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "00000000-0000-0000-0000-000000000000");
+
test(import.meta.url, ({ runBundler, createTempDir }) => {
const tempDir = createTempDir();
runBundler({ SENTRY_TEST_OVERRIDE_TEMP_DIR: tempDir });
- const files = readAllFiles(tempDir);
+ // Rolldown debug IDs are derived from the final chunk text, so they change with every Rolldown
+ // release. Zero them in file names and source maps to keep the snapshot stable.
+ const files = Object.fromEntries(
+ Object.entries(readAllFiles(tempDir, normalizeDebugIds)).map(([name, content]) => [
+ normalizeDebugIds(name),
+ content,
+ ])
+ );
expect(files).toMatchInlineSnapshot(`
{
- "b699d9c1-b033-4536-aa25-233c92609b54-0.js": "//#region src/basic.js
+ "00000000-0000-0000-0000-000000000000-0.js": "//#region src/basic.js
(function() {
try {
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
@@ -23,7 +33,7 @@ test(import.meta.url, ({ runBundler, createTempDir }) => {
//# debugId=00000000-0000-0000-0000-000000000000
//# sourceMappingURL=basic.js.map",
- "b699d9c1-b033-4536-aa25-233c92609b54-0.js.map": "{"version":3,"file":"basic.js","names":[],"sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,cAAc","debugId":"b699d9c1-b033-4536-aa25-233c92609b54","debug_id":"b699d9c1-b033-4536-aa25-233c92609b54"}",
+ "00000000-0000-0000-0000-000000000000-0.js.map": "{"version":3,"file":"basic.js","names":[],"sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,cAAc","debugId":"00000000-0000-0000-0000-000000000000","debug_id":"00000000-0000-0000-0000-000000000000"}",
}
`);
});
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.config.ts
new file mode 100644
index 000000000000..aec427a21800
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.config.ts
@@ -0,0 +1,19 @@
+import { sentryVitePlugin } from "@sentry/bundler-plugins/vite";
+import { defineConfig } from "vite";
+import { sentryConfig } from "../configs/sourcemaps-disable-upload.config.js";
+
+export default defineConfig({
+ build: {
+ minify: false,
+ sourcemap: true,
+ rollupOptions: {
+ input: "src/dynamic-import.js",
+ output: {
+ dir: "out/sourcemaps-disable-upload",
+ entryFileNames: "[name].js",
+ chunkFileNames: "[name].js",
+ },
+ },
+ },
+ plugins: [sentryVitePlugin(sentryConfig)],
+});
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.test.ts
new file mode 100644
index 000000000000..a3c65f8e7755
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/sourcemaps-disable-upload.test.ts
@@ -0,0 +1,25 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { expect } from "vitest";
+import { test } from "./utils";
+
+// Vite rewrites the source map of chunks with dynamic imports after the Sentry plugin stamps it.
+// The debug ID must survive that for every chunk. Files are read raw here because
+// `readOutputFiles` zeroes debug IDs, which would hide a mismatch between chunk and map.
+test(import.meta.url, ({ runBundler, outDir }) => {
+ runBundler();
+
+ const entry = readFileSync(join(outDir, "dynamic-import.js"), "utf-8");
+ expect(entry).toContain("__vitePreload");
+
+ for (const fileName of ["dynamic-import.js", "import.js"]) {
+ const chunk = readFileSync(join(outDir, fileName), "utf-8");
+ const debugId = chunk.match(/sentry-dbid-([0-9a-f-]{36})/)?.[1];
+ const map = JSON.parse(readFileSync(join(outDir, `${fileName}.map`), "utf-8"));
+
+ expect(debugId).toBeDefined();
+ expect(chunk).toContain(`//# debugId=${debugId}`);
+ expect(map.debug_id).toBe(debugId);
+ expect(map.debugId).toBe(debugId);
+ }
+});
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/src/dynamic-import.js b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/src/dynamic-import.js
new file mode 100644
index 000000000000..717d73bf4fe3
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/src/dynamic-import.js
@@ -0,0 +1,5 @@
+export async function load() {
+ return import("./import.js");
+}
+
+load();
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/debugids-already-injected.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/debugids-already-injected.test.ts
index 89189b0338dc..67c935d746f7 100644
--- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/debugids-already-injected.test.ts
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/debugids-already-injected.test.ts
@@ -2,14 +2,24 @@ import { expect } from "vitest";
import { readAllFiles } from "../utils";
import { test } from "./utils";
+const normalizeDebugIds = (value: string): string =>
+ value.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "00000000-0000-0000-0000-000000000000");
+
test(import.meta.url, ({ runBundler, createTempDir }) => {
const tempDir = createTempDir();
runBundler({ SENTRY_TEST_OVERRIDE_TEMP_DIR: tempDir });
- const files = readAllFiles(tempDir);
+ // Rolldown debug IDs are derived from the final chunk text, so they change with every Rolldown
+ // release. Zero them in file names and source maps to keep the snapshot stable.
+ const files = Object.fromEntries(
+ Object.entries(readAllFiles(tempDir, normalizeDebugIds)).map(([name, content]) => [
+ normalizeDebugIds(name),
+ content,
+ ])
+ );
expect(files).toMatchInlineSnapshot(`
{
- "b699d9c1-b033-4536-aa25-233c92609b54-0.js": "//#region src/basic.js
+ "00000000-0000-0000-0000-000000000000-0.js": "//#region src/basic.js
(function() {
try {
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
@@ -23,7 +33,7 @@ test(import.meta.url, ({ runBundler, createTempDir }) => {
//# debugId=00000000-0000-0000-0000-000000000000
//# sourceMappingURL=basic.js.map",
- "b699d9c1-b033-4536-aa25-233c92609b54-0.js.map": "{"version":3,"file":"basic.js","names":[],"sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,aAAa","debugId":"b699d9c1-b033-4536-aa25-233c92609b54","debug_id":"b699d9c1-b033-4536-aa25-233c92609b54"}",
+ "00000000-0000-0000-0000-000000000000-0.js.map": "{"version":3,"file":"basic.js","names":[],"sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,aAAa","debugId":"00000000-0000-0000-0000-000000000000","debug_id":"00000000-0000-0000-0000-000000000000"}",
}
`);
});
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.config.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.config.ts
new file mode 100644
index 000000000000..aec427a21800
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.config.ts
@@ -0,0 +1,19 @@
+import { sentryVitePlugin } from "@sentry/bundler-plugins/vite";
+import { defineConfig } from "vite";
+import { sentryConfig } from "../configs/sourcemaps-disable-upload.config.js";
+
+export default defineConfig({
+ build: {
+ minify: false,
+ sourcemap: true,
+ rollupOptions: {
+ input: "src/dynamic-import.js",
+ output: {
+ dir: "out/sourcemaps-disable-upload",
+ entryFileNames: "[name].js",
+ chunkFileNames: "[name].js",
+ },
+ },
+ },
+ plugins: [sentryVitePlugin(sentryConfig)],
+});
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.test.ts
new file mode 100644
index 000000000000..a3c65f8e7755
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/sourcemaps-disable-upload.test.ts
@@ -0,0 +1,25 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { expect } from "vitest";
+import { test } from "./utils";
+
+// Vite rewrites the source map of chunks with dynamic imports after the Sentry plugin stamps it.
+// The debug ID must survive that for every chunk. Files are read raw here because
+// `readOutputFiles` zeroes debug IDs, which would hide a mismatch between chunk and map.
+test(import.meta.url, ({ runBundler, outDir }) => {
+ runBundler();
+
+ const entry = readFileSync(join(outDir, "dynamic-import.js"), "utf-8");
+ expect(entry).toContain("__vitePreload");
+
+ for (const fileName of ["dynamic-import.js", "import.js"]) {
+ const chunk = readFileSync(join(outDir, fileName), "utf-8");
+ const debugId = chunk.match(/sentry-dbid-([0-9a-f-]{36})/)?.[1];
+ const map = JSON.parse(readFileSync(join(outDir, `${fileName}.map`), "utf-8"));
+
+ expect(debugId).toBeDefined();
+ expect(chunk).toContain(`//# debugId=${debugId}`);
+ expect(map.debug_id).toBe(debugId);
+ expect(map.debugId).toBe(debugId);
+ }
+});
diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/src/dynamic-import.js b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/src/dynamic-import.js
new file mode 100644
index 000000000000..717d73bf4fe3
--- /dev/null
+++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/src/dynamic-import.js
@@ -0,0 +1,5 @@
+export async function load() {
+ return import("./import.js");
+}
+
+load();
diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json
index 162e0a8136d9..2482616b6aa9 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.2.0",
- "@sentry/conventions": "0.24.0",
+ "@sentry/conventions": "0.25.0",
"eslint-plugin-regexp": "^3.1.0",
"prisma": "6.15.0",
"vite": "^8.3.1",
diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/index.ts b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/index.ts
new file mode 100644
index 000000000000..715594b5b401
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/index.ts
@@ -0,0 +1,87 @@
+import * as Sentry from '@sentry/cloudflare';
+import { DurableObject } from 'cloudflare:workers';
+
+interface Env {
+ SENTRY_DSN: string;
+ BLOCK_CONCURRENCY_DURABLE_OBJECT: DurableObjectNamespace;
+ ASYNC_METHOD_DURABLE_OBJECT: DurableObjectNamespace;
+}
+
+export class BlockConcurrencyDurableObject extends DurableObject {
+ private failure?: Error;
+
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env);
+
+ // The callback runs after the constructor has returned, so after the SDK has wrapped `init`.
+ void ctx.blockConcurrencyWhile(async () => {
+ try {
+ this.init();
+ } catch (error) {
+ this.failure = error as Error;
+ }
+ });
+ }
+
+ init(): void {
+ throw new Error('Init failed');
+ }
+
+ ping(): string {
+ const status = this.failure ? 'degraded' : 'ok';
+ Sentry.captureMessage(`block-concurrency-while ping: ${status}`);
+ return status;
+ }
+}
+
+export class AsyncMethodDurableObject extends DurableObject {
+ private failure?: Error;
+ private readonly loaded: Promise;
+
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env);
+
+ this.loaded = this.load();
+ }
+
+ async load(): Promise {
+ // Everything after the first `await` runs after the constructor has returned, so after the SDK
+ // has wrapped `init`.
+ await this.ctx.storage.get('config');
+
+ try {
+ this.init();
+ } catch (error) {
+ this.failure = error as Error;
+ }
+ }
+
+ init(): void {
+ throw new Error('Init failed');
+ }
+
+ async ping(): Promise {
+ await this.loaded;
+ const status = this.failure ? 'degraded' : 'ok';
+ Sentry.captureMessage(`async-method ping: ${status}`);
+ return status;
+ }
+}
+
+export default {
+ async fetch(request: Request, env: Env): Promise {
+ const url = new URL(request.url);
+
+ if (url.pathname === '/block-concurrency-while') {
+ const namespace = env.BLOCK_CONCURRENCY_DURABLE_OBJECT;
+ return new Response(await namespace.get(namespace.idFromName('test')).ping());
+ }
+
+ if (url.pathname === '/async-method') {
+ const namespace = env.ASYNC_METHOD_DURABLE_OBJECT;
+ return new Response(await namespace.get(namespace.idFromName('test')).ping());
+ }
+
+ return new Response('Not found', { status: 404 });
+ },
+} satisfies ExportedHandler;
diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/instrument.server.ts
new file mode 100644
index 000000000000..e577550ae0b8
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/instrument.server.ts
@@ -0,0 +1,6 @@
+import { defineCloudflareOptions } from '@sentry/cloudflare';
+
+export default defineCloudflareOptions((env: { SENTRY_DSN: string }) => ({
+ dsn: env.SENTRY_DSN,
+ tracesSampleRate: 1.0,
+}));
diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts
new file mode 100644
index 000000000000..08d34656ee5b
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/test.ts
@@ -0,0 +1,48 @@
+import type { Event, SerializedStreamedSpanContainer } from '@sentry/core';
+import { SENTRY_OP, SENTRY_ORIGIN, URL_PATH } from '@sentry/conventions/attributes';
+import { expect, it } from 'vitest';
+import { createRunner } from '../../../runner';
+
+// Every envelope of both requests is expected, and `failOnUnexpected` fails the test on any other one.
+// All of them are sent after the constructor work ran, so an error or span from that work would arrive
+// before the runner completes.
+it('does not capture errors a Durable Object catches in work its constructor started', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .unordered()
+ .failOnUnexpected()
+ .expect(envelope => {
+ expect(envelope[1][0][0].type).toBe('event');
+ expect((envelope[1][0][1] as Event).message).toBe('block-concurrency-while ping: degraded');
+ })
+ .expect(envelope => {
+ expect(envelope[1][0][0].type).toBe('event');
+ expect((envelope[1][0][1] as Event).message).toBe('async-method ping: degraded');
+ })
+ .expectN(2, envelope => {
+ expect(envelope[1][0][0].type).toBe('span');
+ const container = envelope[1][0][1] as SerializedStreamedSpanContainer;
+ expect(container.items.map(item => item.name)).toEqual(['ping']);
+ expect(container.items[0]?.attributes[SENTRY_OP]?.value).toBe('rpc');
+ expect(container.items[0]?.attributes[SENTRY_ORIGIN]?.value).toBe('auto.faas.cloudflare.durable_object');
+ })
+ .expect(envelope => {
+ expect(envelope[1][0][0].type).toBe('span');
+ const container = envelope[1][0][1] as SerializedStreamedSpanContainer;
+ expect(container.items.map(item => item.attributes[URL_PATH]?.value)).toEqual(['/block-concurrency-while']);
+ expect(container.items[0]?.attributes[SENTRY_OP]?.value).toBe('http.server');
+ })
+ .expect(envelope => {
+ expect(envelope[1][0][0].type).toBe('span');
+ const container = envelope[1][0][1] as SerializedStreamedSpanContainer;
+ expect(container.items.map(item => item.attributes[URL_PATH]?.value)).toEqual(['/async-method']);
+ expect(container.items[0]?.attributes[SENTRY_OP]?.value).toBe('http.server');
+ })
+ .start(signal);
+
+ const blockConcurrencyResponse = await runner.makeRequest('get', '/block-concurrency-while');
+ const asyncMethodResponse = await runner.makeRequest('get', '/async-method');
+
+ expect(blockConcurrencyResponse).toBe('degraded');
+ expect(asyncMethodResponse).toBe('degraded');
+ await runner.completed();
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/vite.config.mts b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/vite.config.mts
new file mode 100644
index 000000000000..005f4448f6cb
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/vite.config.mts
@@ -0,0 +1,7 @@
+import { cloudflare } from '@cloudflare/vite-plugin';
+import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [cloudflare(), sentryCloudflareVitePlugin()],
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/wrangler.jsonc
new file mode 100644
index 000000000000..952cd3f08583
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/durableobject/constructor-async-work/wrangler.jsonc
@@ -0,0 +1,14 @@
+{
+ "$schema": "../../../node_modules/wrangler/config-schema.json",
+ "name": "durableobject-constructor-async-work-worker",
+ "main": "index.ts",
+ "compatibility_date": "2025-06-17",
+ "compatibility_flags": ["nodejs_compat"],
+ "durable_objects": {
+ "bindings": [
+ { "name": "BLOCK_CONCURRENCY_DURABLE_OBJECT", "class_name": "BlockConcurrencyDurableObject" },
+ { "name": "ASYNC_METHOD_DURABLE_OBJECT", "class_name": "AsyncMethodDurableObject" },
+ ],
+ },
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["BlockConcurrencyDurableObject", "AsyncMethodDurableObject"] }],
+}
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 cb73392fce05..f5a787bbc91e 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,11 +1,8 @@
import {
SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
- SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
- SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
} from '@sentry/core';
import {
SENTRY_SEGMENT_NAME_SOURCE,
@@ -14,6 +11,9 @@ import {
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
+ SENTRY_RELEASE,
+ SENTRY_SDK_INTEGRATIONS,
} from '@sentry/conventions/attributes';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';
@@ -88,9 +88,9 @@ it('sends a streamed span envelope with correct spans for a manually started spa
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName },
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
+ [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
+ [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
},
name: 'test-child-span',
is_segment: false,
@@ -113,8 +113,8 @@ it('sends a streamed span envelope with correct spans for a manually started spa
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName },
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
+ [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
+ [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
},
links: [
{
@@ -150,8 +150,8 @@ it('sends a streamed span envelope with correct spans for a manually started spa
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName },
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
+ [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
+ [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
},
name: 'test-manual-span',
is_segment: false,
@@ -172,9 +172,9 @@ it('sends a streamed span envelope with correct spans for a manually started spa
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentName },
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
+ [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
+ [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
},
name: 'test-span',
is_segment: false,
@@ -192,18 +192,18 @@ it('sends a streamed span envelope with correct spans for a manually started spa
[SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream' },
[SENTRY_SDK_NAME]: { type: 'string', value: CLOUDFLARE_SDK },
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
- [SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: {
+ [SENTRY_SDK_INTEGRATIONS]: {
type: 'array',
value: expect.arrayContaining(['SpanStreaming']),
},
- [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
+ [SENTRY_RELEASE]: { type: 'string', value: '1.0.0' },
[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' },
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 },
[SENTRY_SEGMENT_NAME_SOURCE]: { type: 'string', value: 'route' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
+ [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
'server.address': {
type: 'string',
value: 'localhost',
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/index.ts
new file mode 100644
index 000000000000..947315e12a8e
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/index.ts
@@ -0,0 +1,60 @@
+import Anthropic from '@anthropic-ai/sdk';
+
+// The Sentry Vite plugin instruments this worker at build time, so the `@anthropic-ai/sdk` calls below
+// go through the diagnostics-channel integration (not `instrumentAnthropicAiClient`), the way an app
+// built with the plugin does.
+
+/** A canned SSE body for a streamed message, named after the prompt so a test can tell the calls apart. */
+function streamedMessage(id: string, model: string): string {
+ const events = [
+ {
+ type: 'message_start',
+ message: { id, type: 'message', role: 'assistant', model, content: [], usage: { input_tokens: 10 } },
+ },
+ { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
+ { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'Hello!' } },
+ { type: 'content_block_stop', index: 0 },
+ { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 15 } },
+ { type: 'message_stop' },
+ ];
+ return events.map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('');
+}
+
+const mockFetch: typeof fetch = async (_input, init) => {
+ const body = JSON.parse(String(init?.body)) as { model: string; messages: { content: string }[] };
+ return new Response(streamedMessage(`msg_${body.messages[0]!.content}`, body.model), {
+ status: 200,
+ headers: { 'content-type': 'text/event-stream' },
+ });
+};
+
+export default {
+ async fetch(request) {
+ const client = new Anthropic({ apiKey: 'mock-api-key', fetch: mockFetch });
+ const url = new URL(request.url);
+
+ // `messages.stream()` calls `messages.create({ stream: true })` underneath, tagged with a helper
+ // header. Its own channel covers the call, so the nested create must not get a second span.
+ if (url.pathname === '/stream') {
+ const message = await client.messages
+ .stream({ model: 'claude-3-haiku-20240307', max_tokens: 10, messages: [{ role: 'user', content: 'stream' }] })
+ .finalMessage();
+ return Response.json(message);
+ }
+
+ // `beta.messages.stream()` sends the same header, but no channel covers the beta helper, so its
+ // nested create is the only chance for a span and must keep it.
+ if (url.pathname === '/beta-stream') {
+ const message = await client.beta.messages
+ .stream({
+ model: 'claude-3-haiku-20240307',
+ max_tokens: 10,
+ messages: [{ role: 'user', content: 'beta_stream' }],
+ })
+ .finalMessage();
+ return Response.json(message);
+ }
+
+ return new Response('not found', { status: 404 });
+ },
+} satisfies ExportedHandler;
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/instrument.server.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/instrument.server.ts
new file mode 100644
index 000000000000..2659d1a477a6
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/instrument.server.ts
@@ -0,0 +1,10 @@
+import { defineCloudflareOptions } from '@sentry/cloudflare';
+
+interface Env {
+ SENTRY_DSN: string;
+}
+
+export default defineCloudflareOptions((env: Env) => ({
+ dsn: env.SENTRY_DSN,
+ tracesSampleRate: 1.0,
+}));
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/test.ts
new file mode 100644
index 000000000000..09c194076af4
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/test.ts
@@ -0,0 +1,40 @@
+import { GEN_AI_RESPONSE_ID, GEN_AI_RESPONSE_STREAMING } from '@sentry/conventions/attributes';
+import { expect, it } from 'vitest';
+import { createRunner } from '../../../runner';
+import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
+
+// The stream helpers of the `@anthropic-ai/sdk` tag their internal `create` with a helper header, and
+// the integration skips that create only while one of its own `messages.stream()` spans is active.
+// This runs that check on workerd, where the active span is the core span rather than an
+// OpenTelemetry one, through the Vite plugin's build-time instrumentation.
+
+it('emits one span for messages.stream(), not a second one for its internal create', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .ignore('event')
+ .expect(envelope => {
+ const spans = getSpansFromEnvelope(envelope);
+ const genAiSpans = spans.filter(span => getSpanOp(span)?.startsWith('gen_ai.'));
+
+ expect(genAiSpans).toHaveLength(1);
+ expect(genAiSpans[0]!.attributes[GEN_AI_RESPONSE_ID]).toEqual({ value: 'msg_stream', type: 'string' });
+ expect(genAiSpans[0]!.attributes[GEN_AI_RESPONSE_STREAMING]).toEqual({ value: true, type: 'boolean' });
+ })
+ .start(signal);
+ await runner.makeRequest('get', '/stream');
+ await runner.completed();
+});
+
+it('emits one span for beta.messages.stream(), whose internal create is the only span', async ({ signal }) => {
+ const runner = createRunner(__dirname)
+ .ignore('event')
+ .expect(envelope => {
+ const spans = getSpansFromEnvelope(envelope);
+ const genAiSpans = spans.filter(span => getSpanOp(span)?.startsWith('gen_ai.'));
+
+ expect(genAiSpans).toHaveLength(1);
+ expect(genAiSpans[0]!.attributes[GEN_AI_RESPONSE_ID]).toEqual({ value: 'msg_beta_stream', type: 'string' });
+ })
+ .start(signal);
+ await runner.makeRequest('get', '/beta-stream');
+ await runner.completed();
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/vite.config.mts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/vite.config.mts
new file mode 100644
index 000000000000..005f4448f6cb
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/vite.config.mts
@@ -0,0 +1,7 @@
+import { cloudflare } from '@cloudflare/vite-plugin';
+import { sentryCloudflareVitePlugin } from '@sentry/cloudflare/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [cloudflare(), sentryCloudflareVitePlugin()],
+});
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/wrangler.jsonc b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/wrangler.jsonc
new file mode 100644
index 000000000000..25b432ac5751
--- /dev/null
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai-stream-helper/wrangler.jsonc
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/wrangler/config-schema.json",
+ "name": "cloudflare-anthropic-ai-stream-helper",
+ "main": "index.ts",
+ "compatibility_date": "2025-06-17",
+ "compatibility_flags": ["nodejs_compat"],
+}
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts
index b68a41258aa0..854c492fc890 100644
--- a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts
@@ -17,8 +17,9 @@ import {
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
} from '@sentry/conventions/attributes';
-import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT } from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
import { createRunner } from '../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
@@ -71,7 +72,7 @@ it('traces a basic message creation request with the anthropic SDK', async ({ si
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
});
})
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 5b301a9f29ae..2cd081b06b51 100644
--- a/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/d1/test.ts
@@ -1,17 +1,13 @@
import { expect, it } from 'vitest';
import type { Envelope, SerializedStreamedSpan } from '@sentry/core';
-import {
- SDK_VERSION,
- SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
- SEMANTIC_ATTRIBUTE_SENTRY_OP,
- SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
-} from '@sentry/core';
+import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import {
SENTRY_SDK_NAME,
SENTRY_SDK_VERSION,
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
} from '@sentry/conventions/attributes';
import { createRunner } from '../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
@@ -38,7 +34,7 @@ function commonAttributes(segmentSpan: SerializedStreamedSpan): SerializedStream
[SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION },
[SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpan.span_id },
[SENTRY_SEGMENT_NAME]: { type: 'string', value: segmentSpan.name },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
+ [SENTRY_ENVIRONMENT]: { type: 'string', value: 'production' },
[SEMANTIC_ATTRIBUTE_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/google-genai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/test.ts
index 9e8f12d922ba..27fc478b4667 100644
--- a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/test.ts
@@ -24,8 +24,9 @@ import {
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
} from '@sentry/conventions/attributes';
-import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT } from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
import { createRunner } from '../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
@@ -98,7 +99,7 @@ it('traces Google GenAI chat, generateContent, and embedContent calls', async ({
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
});
@@ -138,7 +139,7 @@ it('traces Google GenAI chat, generateContent, and embedContent calls', async ({
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
});
@@ -164,7 +165,7 @@ it('traces Google GenAI chat, generateContent, and embedContent calls', async ({
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
});
})
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts
index 7b4de87424a4..e260bfeff533 100644
--- a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts
@@ -16,8 +16,9 @@ import {
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
} from '@sentry/conventions/attributes';
-import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT } from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
import { GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE } from '../../../../../packages/server-utils/src/ai/core/gen-ai-attributes';
import { createRunner } from '../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
@@ -67,7 +68,7 @@ it('traces a LangChain chat model invocation', async ({ signal }) => {
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
});
})
diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts
index 0a031031a7d7..fafce0225c1e 100644
--- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts
+++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts
@@ -17,8 +17,9 @@ import {
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
} from '@sentry/conventions/attributes';
-import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT } from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
import { createRunner } from '../../../runner';
import { getSpanOp, getSpansFromEnvelope } from '../../../spanUtils';
@@ -74,7 +75,7 @@ it('traces a basic chat completion request with the openai SDK', async ({ signal
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
});
})
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 e48428c9c732..42d2d895fea0 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
@@ -34,6 +34,14 @@ export default Sentry.withSentry(
return new Response(JSON.stringify(result));
}
+ if (url.pathname === '/evaluate') {
+ const result = await ai.run('typesafe/jev', {
+ state: 'Help! My payouts have been failing for 3 days.',
+ questions: { is_urgent: { type: 'noul', instructions: 'Does this convey urgency?' } },
+ });
+ 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 bce096d4ae75..e0a26d02bf86 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
@@ -23,6 +23,17 @@ export class MockAi {
throw error;
}
+ if (model === 'typesafe/jev') {
+ return {
+ state: 'Completed',
+ result: {
+ model: 'jev-1.13.0',
+ answers: { is_urgent: { type: 'noul', noul: 0.97 } },
+ usage: { input_tokens: 426, output_tokens: 73 },
+ },
+ };
+ }
+
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 726f91a5e467..6ed31514dfe4 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
@@ -5,6 +5,7 @@ import {
GEN_AI_REQUEST_MAX_TOKENS,
GEN_AI_REQUEST_MODEL,
GEN_AI_REQUEST_TEMPERATURE,
+ GEN_AI_RESPONSE_MODEL,
GEN_AI_RESPONSE_STREAMING,
GEN_AI_RESPONSE_TEXT,
GEN_AI_USAGE_INPUT_TOKENS,
@@ -15,8 +16,9 @@ import {
SENTRY_SEGMENT_ID,
SENTRY_SEGMENT_NAME,
SENTRY_TRACE_LIFECYCLE,
+ SENTRY_ENVIRONMENT,
} from '@sentry/conventions/attributes';
-import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT } from '@sentry/core';
+import { SDK_VERSION } from '@sentry/core';
import { expect, it } from 'vitest';
import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../../../../../packages/server-utils/src/ai/core/gen-ai-attributes';
import { createRunner } from '../../../runner';
@@ -68,7 +70,7 @@ it('traces a basic Workers AI text generation request', async ({ signal }) => {
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
}),
);
@@ -122,7 +124,7 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) =
[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' },
- [SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
+ [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' },
},
}),
);
@@ -132,6 +134,52 @@ it('traces a streaming Workers AI text generation request', async ({ signal }) =
await runner.completed();
});
+it('traces a TypeSafe Jev evaluation like the TypeSafe integration', 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 typesafe/jev',
+ 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: 'typesafe/jev', type: 'string' },
+ [GEN_AI_RESPONSE_MODEL]: { value: 'jev-1.13.0', type: 'string' },
+ [GEN_AI_USAGE_INPUT_TOKENS]: { value: 426, type: 'integer' },
+ [GEN_AI_USAGE_OUTPUT_TOKENS]: { value: 73, type: 'integer' },
+ [GEN_AI_USAGE_TOTAL_TOKENS]: { value: 499, type: 'integer' },
+ // collect only output messages
+ [GEN_AI_OUTPUT_MESSAGES]: {
+ type: 'string',
+ value: '[{"type":"evaluation","answers":{"is_urgent":{"type":"noul","noul":0.97}}}]',
+ },
+ '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');
+ 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/deno-integration-tests/node-suites/excludes.ts b/dev-packages/deno-integration-tests/node-suites/excludes.ts
index e2b5e0cd5b01..36098da333df 100644
--- a/dev-packages/deno-integration-tests/node-suites/excludes.ts
+++ b/dev-packages/deno-integration-tests/node-suites/excludes.ts
@@ -49,6 +49,7 @@ const REQUIRE_OF_ESM_ONLY_DEPENDENCY = [
// version) and pass on Deno 2.9.0.
const NOT_TRIAGED = [
'suites/tracing/anthropic/test.ts',
+ 'suites/tracing/anthropic/v0.129/test.ts',
'suites/tracing/apollo-graphql/**',
'suites/tracing/fastify/test.ts',
'suites/tracing/flue/test.ts',
diff --git a/dev-packages/deno-integration-tests/src/index.ts b/dev-packages/deno-integration-tests/src/index.ts
index 5268dfd42a8a..3c18c1154838 100644
--- a/dev-packages/deno-integration-tests/src/index.ts
+++ b/dev-packages/deno-integration-tests/src/index.ts
@@ -72,6 +72,11 @@ export function transactionSink(): {
};
}
+export function getSpanOp(span: SerializedStreamedSpan): string | undefined {
+ const attribute = span.attributes['sentry.op'];
+ return attribute?.type === 'string' ? attribute.value : undefined;
+}
+
/**
* A `beforeSend` hook that records every error and lets a test
* `await` the first one matching a predicate. `waitFor` resolves immediately if
@@ -98,18 +103,22 @@ export function withTimeout(p: Promise, ms: number, what: string): Promise
* (returning null is disallowed), so intercepting them needs a transport rather than a hook.
*/
export function spanSink(): {
+ spans: SerializedStreamedSpan[];
waitFor: (predicate: (span: SerializedStreamedSpan) => boolean) => Promise;
transport: () => Transport;
} {
+ const spans: SerializedStreamedSpan[] = [];
const sink = eventSink();
return {
+ spans,
waitFor: sink.waitFor,
transport: () => ({
send: (envelope: Envelope) => {
for (const [header, payload] of envelope[1]) {
if (header.type === 'span') {
for (const span of (payload as SerializedStreamedSpanContainer).items) {
+ spans.push(span);
sink.beforeSend(span);
}
}
diff --git a/dev-packages/deno-integration-tests/suites/deno-serve-request-bodies/test.ts b/dev-packages/deno-integration-tests/suites/deno-serve-request-bodies/test.ts
index 49e732281bde..53f0c55877f8 100644
--- a/dev-packages/deno-integration-tests/suites/deno-serve-request-bodies/test.ts
+++ b/dev-packages/deno-integration-tests/suites/deno-serve-request-bodies/test.ts
@@ -1,18 +1,17 @@
//
-import { denoServeIntegration, init } from '@sentry/deno';
+import { denoServeIntegration, flush, init } from '@sentry/deno';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('Deno.serve captures incoming request bodies by default', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const requestBody = 'captured-by-default';
@@ -25,31 +24,37 @@ Deno.test('Deno.serve captures incoming request bodies by default', async () =>
});
await listening;
- const transactionPromise = withTimeout(
- sink.waitFor(event => event.request?.url?.endsWith('/default') === true),
- 5_000,
- 'transaction for /default',
- );
- const response = await fetch(`http://localhost:${server.addr.port}/default`, {
- method: 'POST',
- headers: { 'content-type': 'text/plain' },
- body: requestBody,
- });
- assertEquals(await response.text(), 'OK');
-
- const transaction = await transactionPromise;
- assertEquals(transaction.request?.data, requestBody);
-
- abortController.abort();
- await server.finished;
+ try {
+ const spanPromise = withTimeout(
+ sink.waitFor(
+ span =>
+ span.is_segment && getSpanOp(span) === 'http.server' && span.attributes['url.path']?.value === '/default',
+ ),
+ 5_000,
+ 'segment span for /default',
+ );
+ const response = await fetch(`http://localhost:${server.addr.port}/default`, {
+ method: 'POST',
+ headers: { 'content-type': 'text/plain' },
+ body: requestBody,
+ });
+ assertEquals(await response.text(), 'OK');
+
+ await flush();
+ const span = await spanPromise;
+ assertEquals(span.name, 'POST');
+ assertEquals(span.attributes['http.request.body.data']?.value, requestBody);
+ } finally {
+ abortController.abort();
+ await server.finished;
+ }
});
Deno.test('Deno.serve explicit small overrides disabled incoming request body collection', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
dataCollection: { httpBodies: [] },
@@ -57,7 +62,7 @@ Deno.test('Deno.serve explicit small overrides disabled incoming request body co
...integrations.filter(integration => integration.name !== 'DenoServe'),
denoServeIntegration({ maxRequestBodySize: 'small' }),
],
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const requestBody = 'a'.repeat(1_001);
@@ -71,31 +76,39 @@ Deno.test('Deno.serve explicit small overrides disabled incoming request body co
});
await listening;
- const transactionPromise = withTimeout(
- sink.waitFor(event => event.request?.url?.endsWith('/explicit-small') === true),
- 5_000,
- 'transaction for /explicit-small',
- );
- const response = await fetch(`http://localhost:${server.addr.port}/explicit-small`, {
- method: 'POST',
- headers: { 'content-type': 'text/plain' },
- body: requestBody,
- });
- assertEquals(await response.text(), 'OK');
-
- const transaction = await transactionPromise;
- assertEquals(transaction.request?.data, expectedBody);
-
- abortController.abort();
- await server.finished;
+ try {
+ const spanPromise = withTimeout(
+ sink.waitFor(
+ span =>
+ span.is_segment &&
+ getSpanOp(span) === 'http.server' &&
+ span.attributes['url.path']?.value === '/explicit-small',
+ ),
+ 5_000,
+ 'segment span for /explicit-small',
+ );
+ const response = await fetch(`http://localhost:${server.addr.port}/explicit-small`, {
+ method: 'POST',
+ headers: { 'content-type': 'text/plain' },
+ body: requestBody,
+ });
+ assertEquals(await response.text(), 'OK');
+
+ await flush();
+ const span = await spanPromise;
+ assertEquals(span.name, 'POST');
+ assertEquals(span.attributes['http.request.body.data']?.value, expectedBody);
+ } finally {
+ abortController.abort();
+ await server.finished;
+ }
});
Deno.test('Deno.serve explicit none overrides enabled incoming request body collection', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
dataCollection: { httpBodies: ['incomingRequest'] },
@@ -103,7 +116,7 @@ Deno.test('Deno.serve explicit none overrides enabled incoming request body coll
...integrations.filter(integration => integration.name !== 'DenoServe'),
denoServeIntegration({ maxRequestBodySize: 'none' }),
],
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const abortController = new AbortController();
@@ -115,21 +128,30 @@ Deno.test('Deno.serve explicit none overrides enabled incoming request body coll
});
await listening;
- const transactionPromise = withTimeout(
- sink.waitFor(event => event.request?.url?.endsWith('/explicit-none') === true),
- 5_000,
- 'transaction for /explicit-none',
- );
- const response = await fetch(`http://localhost:${server.addr.port}/explicit-none`, {
- method: 'POST',
- headers: { 'content-type': 'text/plain' },
- body: 'do-not-capture',
- });
- assertEquals(await response.text(), 'OK');
-
- const transaction = await transactionPromise;
- assertEquals(transaction.request?.data, undefined);
-
- abortController.abort();
- await server.finished;
+ try {
+ const spanPromise = withTimeout(
+ sink.waitFor(
+ span =>
+ span.is_segment &&
+ getSpanOp(span) === 'http.server' &&
+ span.attributes['url.path']?.value === '/explicit-none',
+ ),
+ 5_000,
+ 'segment span for /explicit-none',
+ );
+ const response = await fetch(`http://localhost:${server.addr.port}/explicit-none`, {
+ method: 'POST',
+ headers: { 'content-type': 'text/plain' },
+ body: 'do-not-capture',
+ });
+ assertEquals(await response.text(), 'OK');
+
+ await flush();
+ const span = await spanPromise;
+ assertEquals(span.name, 'POST');
+ assertEquals(span.attributes['http.request.body.data']?.value, undefined);
+ } finally {
+ abortController.abort();
+ await server.finished;
+ }
});
diff --git a/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs b/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs
index 1dc48ebb60c2..3b045ec0829e 100644
--- a/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs
+++ b/dev-packages/deno-integration-tests/suites/direct-client-acs/scenario.mjs
@@ -12,22 +12,24 @@ import { nodeStackLineParser } from '@sentry/core/server';
import { DenoClient, getCurrentScope, getDefaultIntegrations, startSpan } from '@sentry/deno';
import { tracingChannel } from 'node:diagnostics_channel';
-let nested = false;
+const spans = [];
const client = new DenoClient({
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- traceLifecycle: 'static',
integrations: getDefaultIntegrations({ tracesSampleRate: 1 }),
stackParser: createStackParser(nodeStackLineParser()),
- beforeSendTransaction(event) {
- const spans = event.spans ?? [];
- if (spans.some(s => s.op === 'db' && s.data?.['sentry.origin'] === 'auto.db.mysql')) {
- nested = true;
- }
- return null;
- },
- transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }),
+ transport: () => ({
+ send(envelope) {
+ for (const [header, payload] of envelope[1]) {
+ if (header.type === 'span') {
+ spans.push(...payload.items);
+ }
+ }
+ return Promise.resolve({});
+ },
+ flush: () => Promise.resolve(true),
+ }),
});
client.init();
@@ -50,5 +52,14 @@ startSpan({ name: 'parent', op: 'test' }, () => {
await client.flush(2000);
+const parent = spans.find(span => span.is_segment && span.name === 'parent');
+const nested = spans.some(
+ span =>
+ span.attributes['sentry.op']?.value === 'db' &&
+ span.attributes['sentry.origin']?.value === 'auto.db.mysql' &&
+ span.parent_span_id === parent?.span_id &&
+ span.trace_id === parent?.trace_id,
+);
+
// eslint-disable-next-line no-console
console.log(`SCENARIO nested=${nested}`);
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts
index d059cb76971c..9c1031ed9ee3 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-amqplib/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('amqplib instrumentation: included in default integrations (Deno 2.8.0
Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces a nested queue.publish span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:amqplib:publish');
@@ -47,16 +45,25 @@ Deno.test('amqplib instrumentation: orchestrion:amqplib:publish channel produces
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const publishSpan = parent.spans?.find(s => s.op === 'queue.publish');
- assertExists(publishSpan, `expected a queue.publish child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(publishSpan!.description, 'publish my-exchange');
- assertEquals(publishSpan!.data?.['messaging.destination.name'], 'my-exchange');
- assertEquals(publishSpan!.data?.['messaging.system'], 'rabbitmq');
- assertEquals(publishSpan!.data?.['sentry.origin'], 'auto.amqplib.publisher');
+ const publishSpan = children.find(s => getSpanOp(s) === 'queue.publish');
+ assertExists(
+ publishSpan,
+ `expected a queue.publish child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
+ );
+ assertEquals(publishSpan.name, 'send my-exchange');
+ assertEquals(publishSpan.attributes['messaging.destination.name']?.value, 'my-exchange');
+ assertEquals(publishSpan.attributes['messaging.system']?.value, 'rabbitmq');
+ assertEquals(publishSpan.attributes['sentry.origin']?.value, 'auto.amqplib.publisher');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-anthropic/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-anthropic/test.ts
index 9372fd70934f..d4dc830311fc 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-anthropic/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-anthropic/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('anthropic instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('anthropic instrumentation: included in default integrations (Deno 2.8
Deno.test('anthropic instrumentation: orchestrion @anthropic-ai/sdk:chat channel produces a nested gen_ai span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:@anthropic-ai/sdk:chat');
@@ -46,19 +44,25 @@ Deno.test('anthropic instrumentation: orchestrion @anthropic-ai/sdk:chat channel
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.chat');
- assertExists(aiSpan, `expected a gen_ai.chat child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(aiSpan!.description, 'chat claude-3-5-sonnet-latest');
- assertEquals(aiSpan!.data?.['gen_ai.provider.name'], 'anthropic');
- assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'chat');
- assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'claude-3-5-sonnet-latest');
- assertEquals(aiSpan!.data?.['gen_ai.response.model'], 'claude-3-5-sonnet-20241022');
- assertEquals(aiSpan!.data?.['gen_ai.usage.total_tokens'], 15);
- assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.anthropic');
+ const aiSpan = children.find(s => getSpanOp(s) === 'gen_ai.chat');
+ assertExists(aiSpan, `expected a gen_ai.chat child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(aiSpan.name, 'chat claude-3-5-sonnet-latest');
+ assertEquals(aiSpan.attributes['gen_ai.provider.name']?.value, 'anthropic');
+ assertEquals(aiSpan.attributes['gen_ai.operation.name']?.value, 'chat');
+ assertEquals(aiSpan.attributes['gen_ai.request.model']?.value, 'claude-3-5-sonnet-latest');
+ assertEquals(aiSpan.attributes['gen_ai.response.model']?.value, 'claude-3-5-sonnet-20241022');
+ assertEquals(aiSpan.attributes['gen_ai.usage.total_tokens']?.value, 15);
+ assertEquals(aiSpan.attributes['sentry.origin']?.value, 'auto.ai.anthropic');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-aws/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-aws/test.ts
index b07fb3da9b31..1beb142f3313 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-aws/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-aws/test.ts
@@ -3,17 +3,16 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { Span } from '@sentry/core';
import type { DenoClient } from '@sentry/deno';
-import { init, spanToJSON, startSpan } from '@sentry/deno';
+import { flush, init, spanToJSON, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
import { SENTRY_OP } from '@sentry/conventions/attributes';
Deno.test('aws-sdk instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -29,17 +28,16 @@ Deno.test('aws-sdk instrumentation: included in default integrations (Deno 2.8.0
// the parent span is held open until the `spanEnd` hook reports the child ended.
Deno.test('aws-sdk instrumentation: orchestrion @smithy/smithy-client:send channel produces a nested rpc span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
}) as DenoClient;
// The rpc span ends only after the deferred region backfill settles. Wait on the
// concrete `spanEnd` signal rather than a timer, so the parent stays open until the
- // child has actually ended and can be captured on the transaction.
+ // child has actually ended before flushing the span buffer.
const rpcSpanEnded = new Promise(resolve => {
client.on('spanEnd', (span: Span) => {
if (spanToJSON(span).attributes[SENTRY_OP] === 'rpc') {
@@ -71,18 +69,24 @@ Deno.test('aws-sdk instrumentation: orchestrion @smithy/smithy-client:send chann
await rpcSpanEnded;
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const awsSpan = parent.spans?.find(s => s.op === 'rpc');
- assertExists(awsSpan, `expected an rpc child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(awsSpan!.description, 'CloudWatch.DescribeAlarms');
- assertEquals(awsSpan!.data?.['rpc.system'], 'aws-api');
- assertEquals(awsSpan!.data?.['rpc.service'], 'CloudWatch');
- assertEquals(awsSpan!.data?.['rpc.method'], 'DescribeAlarms');
- assertEquals(awsSpan!.data?.['cloud.region'], 'us-east-1');
- assertEquals(awsSpan!.data?.['sentry.origin'], 'auto.aws.aws_sdk');
+ const awsSpan = children.find(s => getSpanOp(s) === 'rpc');
+ assertExists(awsSpan, `expected an rpc child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(awsSpan.name, 'CloudWatch.DescribeAlarms');
+ assertEquals(awsSpan.attributes['rpc.system']?.value, 'aws-api');
+ assertEquals(awsSpan.attributes['rpc.service']?.value, 'CloudWatch');
+ assertEquals(awsSpan.attributes['rpc.method']?.value, 'DescribeAlarms');
+ assertEquals(awsSpan.attributes['cloud.region']?.value, 'us-east-1');
+ assertEquals(awsSpan.attributes['sentry.origin']?.value, 'auto.aws.aws_sdk');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-express/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-express/test.ts
index 7fb2aafd0bf6..1b480f847fae 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-express/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-express/test.ts
@@ -3,27 +3,26 @@
import { EventEmitter } from 'node:events';
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('express instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
- const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient;
+ const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Express'), `Express should be in defaults, got ${names.join(', ')}`);
});
Deno.test('express instrumentation: orchestrion:express:handle channel produces a nested middleware span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:express:handle');
@@ -40,16 +39,25 @@ Deno.test('express instrumentation: orchestrion:express:handle channel produces
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
-
- const expressSpan = parent.spans?.find(s => s.op === 'middleware');
- assertExists(expressSpan, `expected an express middleware span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(expressSpan!.description, 'myMiddleware');
- assertEquals(expressSpan!.data?.['express.name'], 'myMiddleware');
- assertEquals(expressSpan!.data?.['express.type'], 'middleware');
- assertEquals(expressSpan!.data?.['sentry.origin'], 'auto.http.express');
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
+
+ const expressSpan = children.find(s => getSpanOp(s) === 'middleware');
+ assertExists(
+ expressSpan,
+ `expected an express middleware span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
+ );
+ assertEquals(expressSpan.name, 'myMiddleware');
+ assertEquals(expressSpan.attributes['express.name']?.value, 'myMiddleware');
+ assertEquals(expressSpan.attributes['express.type']?.value, 'middleware');
+ assertEquals(expressSpan.attributes['sentry.origin']?.value, 'auto.http.express');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts
index 276a74eda91c..a6f0f4fbdcd1 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-fastify/test.ts
@@ -10,7 +10,7 @@ import { errorSink, resetGlobals, withTimeout } from '../../src/index.ts';
Deno.test('fastify instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
- const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient;
+ const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Fastify'), `Fastify should be in defaults, got ${names.join(', ')}`);
});
@@ -19,7 +19,6 @@ Deno.test('fastify instrumentation: tracing:fastify.request.handler:error channe
resetGlobals();
const sink = errorSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
beforeSend: sink.beforeSend,
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-firebase/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-firebase/test.ts
index 6d4d3e6ff87a..4f6440b8a677 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-firebase/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-firebase/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('firebase instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('firebase instrumentation: included in default integrations (Deno 2.8.
Deno.test('firebase instrumentation: orchestrion @firebase/firestore:add-doc channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:@firebase/firestore:add-doc');
@@ -52,19 +50,25 @@ Deno.test('firebase instrumentation: orchestrion @firebase/firestore:add-doc cha
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const fsSpan = parent.spans?.find(s => s.op === 'db.query');
- assertExists(fsSpan, `expected a db.query child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(fsSpan!.description, 'addDoc users');
- assertEquals(fsSpan!.data?.['db.operation.name'], 'addDoc');
- assertEquals(fsSpan!.data?.['db.collection.name'], 'users');
- assertEquals(fsSpan!.data?.['db.namespace'], '[DEFAULT]');
- assertEquals(fsSpan!.data?.['db.system.name'], 'firebase.firestore');
- assertEquals(fsSpan!.data?.['firebase.firestore.options.projectId'], 'demo-project');
- assertEquals(fsSpan!.data?.['sentry.origin'], 'auto.firebase.firestore');
+ const fsSpan = children.find(s => getSpanOp(s) === 'db.query');
+ assertExists(fsSpan, `expected a db.query child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(fsSpan.name, 'addDoc users');
+ assertEquals(fsSpan.attributes['db.operation.name']?.value, 'addDoc');
+ assertEquals(fsSpan.attributes['db.collection.name']?.value, 'users');
+ assertEquals(fsSpan.attributes['db.namespace']?.value, '[DEFAULT]');
+ assertEquals(fsSpan.attributes['db.system.name']?.value, 'firebase.firestore');
+ assertEquals(fsSpan.attributes['firebase.firestore.options.projectId']?.value, 'demo-project');
+ assertEquals(fsSpan.attributes['sentry.origin']?.value, 'auto.firebase.firestore');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-generic-pool/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-generic-pool/test.ts
index 2f5258935c00..4733848fe2e3 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-generic-pool/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-generic-pool/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('generic-pool instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('generic-pool instrumentation: included in default integrations (Deno
Deno.test('generic-pool instrumentation: orchestrion:generic-pool:acquire channel produces a nested span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:generic-pool:acquire');
@@ -43,17 +41,20 @@ Deno.test('generic-pool instrumentation: orchestrion:generic-pool:acquire channe
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
- );
-
- const poolSpan = parent.spans?.find(s => s.description === 'generic-pool.acquire');
- assertExists(
- poolSpan,
- `expected a generic-pool.acquire span, got descriptions: ${parent.spans?.map(s => s.description).join(', ')}`,
+ "'parent' segment span",
);
- assertEquals(poolSpan!.data?.['sentry.origin'], 'auto.db.generic_pool');
- assertEquals(poolSpan!.op, 'db');
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
+
+ const poolSpan = children.find(s => s.name === 'generic-pool.acquire');
+ assertExists(poolSpan, `expected a generic-pool.acquire span, got names: ${children.map(s => s.name).join(', ')}`);
+ assertEquals(poolSpan.attributes['sentry.origin']?.value, 'auto.db.generic_pool');
+ assertEquals(getSpanOp(poolSpan), 'db');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-google-genai/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-google-genai/test.ts
index db193197e0f5..e28930b42161 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-google-genai/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-google-genai/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('google-genai instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('google-genai instrumentation: included in default integrations (Deno
Deno.test('google-genai instrumentation: orchestrion @google/genai:generate-content channel produces a nested gen_ai span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:@google/genai:generate-content');
@@ -45,22 +43,28 @@ Deno.test('google-genai instrumentation: orchestrion @google/genai:generate-cont
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.generate_content');
+ const aiSpan = children.find(s => getSpanOp(s) === 'gen_ai.generate_content');
assertExists(
aiSpan,
- `expected a gen_ai.generate_content child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
+ `expected a gen_ai.generate_content child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
);
- assertEquals(aiSpan!.description, 'generate_content gemini-1.5-flash');
- assertEquals(aiSpan!.data?.['gen_ai.provider.name'], 'google_genai');
- assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'generate_content');
- assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'gemini-1.5-flash');
- assertEquals(aiSpan!.data?.['gen_ai.response.model'], 'gemini-1.5-flash-002');
- assertEquals(aiSpan!.data?.['gen_ai.usage.total_tokens'], 15);
- assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.google_genai');
+ assertEquals(aiSpan.name, 'generate_content gemini-1.5-flash');
+ assertEquals(aiSpan.attributes['gen_ai.provider.name']?.value, 'google_genai');
+ assertEquals(aiSpan.attributes['gen_ai.operation.name']?.value, 'generate_content');
+ assertEquals(aiSpan.attributes['gen_ai.request.model']?.value, 'gemini-1.5-flash');
+ assertEquals(aiSpan.attributes['gen_ai.response.model']?.value, 'gemini-1.5-flash-002');
+ assertEquals(aiSpan.attributes['gen_ai.usage.total_tokens']?.value, 15);
+ assertEquals(aiSpan.attributes['sentry.origin']?.value, 'auto.ai.google_genai');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-graphql/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-graphql/test.ts
index a352bd20b79d..2497b4f6eef9 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-graphql/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-graphql/test.ts
@@ -16,6 +16,7 @@ async function assertParseSpan(channelName: string): Promise {
resetGlobals();
const sink = transactionSink();
init({
+ // Keep one integration suite on the static lifecycle as a regression guard.
traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
@@ -49,7 +50,6 @@ async function assertParseSpan(channelName: string): Promise {
Deno.test('graphql instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts
index af0a9c92a0c4..28a1c2e49f06 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-hapi/test.ts
@@ -2,27 +2,26 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('hapi instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
- const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient;
+ const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Hapi'), `Hapi should be in defaults, got ${names.join(', ')}`);
});
Deno.test('hapi instrumentation: orchestrion:@hapi/hapi:route channel wraps the route handler into a span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
// `start` wraps the route's `handler` in place; the span opens when that
@@ -36,16 +35,22 @@ Deno.test('hapi instrumentation: orchestrion:@hapi/hapi:route channel wraps the
wrappedRoute.handler({}, {});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const hapiSpan = parent.spans?.find(s => s.op === 'router');
- assertExists(hapiSpan, `expected a router span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(hapiSpan!.description, 'GET /hello');
- assertEquals(hapiSpan!.data?.['hapi.type'], 'router');
- assertEquals(hapiSpan!.data?.['http.route'], '/hello');
- assertEquals(hapiSpan!.data?.['sentry.origin'], 'auto.http.hapi');
+ const hapiSpan = children.find(s => getSpanOp(s) === 'router');
+ assertExists(hapiSpan, `expected a router span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(hapiSpan.name, '/hello');
+ assertEquals(hapiSpan.attributes['hapi.type']?.value, 'router');
+ assertEquals(hapiSpan.attributes['http.route']?.value, '/hello');
+ assertEquals(hapiSpan.attributes['sentry.origin']?.value, 'auto.http.hapi');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-kafkajs/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-kafkajs/test.ts
index d94839209bd1..0265468722a3 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-kafkajs/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-kafkajs/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('kafkajs instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('kafkajs instrumentation: included in default integrations (Deno 2.8.0
Deno.test('kafkajs instrumentation: orchestrion:kafkajs:send_batch channel produces a nested producer span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:kafkajs:send_batch');
@@ -39,16 +37,25 @@ Deno.test('kafkajs instrumentation: orchestrion:kafkajs:send_batch channel produ
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
-
- const kafkaSpan = parent.spans?.find(s => s.op === 'queue.publish');
- assertExists(kafkaSpan, `expected a queue.publish child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(kafkaSpan!.description, 'send my-topic');
- assertEquals(kafkaSpan!.data?.['messaging.system'], 'kafka');
- assertEquals(kafkaSpan!.data?.['messaging.destination.name'], 'my-topic');
- assertEquals(kafkaSpan!.data?.['sentry.origin'], 'auto.kafkajs.producer');
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
+
+ const kafkaSpan = children.find(s => getSpanOp(s) === 'queue.publish');
+ assertExists(
+ kafkaSpan,
+ `expected a queue.publish child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
+ );
+ assertEquals(kafkaSpan.name, 'send my-topic');
+ assertEquals(kafkaSpan.attributes['messaging.system']?.value, 'kafka');
+ assertEquals(kafkaSpan.attributes['messaging.destination.name']?.value, 'my-topic');
+ assertEquals(kafkaSpan.attributes['sentry.origin']?.value, 'auto.kafkajs.producer');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts
index e084e4cd136a..d7bb9b689efa 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-koa/test.ts
@@ -2,27 +2,26 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('koa instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
- const client = init({ traceLifecycle: 'static', dsn: 'https://username@domain/123' }) as DenoClient;
+ const client = init({ dsn: 'https://username@domain/123' }) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('Koa'), `Koa should be in defaults, got ${names.join(', ')}`);
});
Deno.test('koa instrumentation: orchestrion:koa:use channel wraps middleware into a span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
function myMiddleware(_context: unknown, next: () => Promise): Promise {
@@ -38,14 +37,20 @@ Deno.test('koa instrumentation: orchestrion:koa:use channel wraps middleware int
await wrappedMiddleware({}, () => Promise.resolve());
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
-
- const koaSpan = parent.spans?.find(s => s.op === 'middleware');
- assertExists(koaSpan, `expected a koa middleware child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(koaSpan!.description, 'myMiddleware');
- assertEquals(koaSpan!.data?.['sentry.origin'], 'auto.http.koa');
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
+
+ const koaSpan = children.find(s => getSpanOp(s) === 'middleware');
+ assertExists(koaSpan, `expected a koa middleware child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(koaSpan.name, 'myMiddleware');
+ assertEquals(koaSpan.attributes['sentry.origin']?.value, 'auto.http.koa');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-langchain/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-langchain/test.ts
index 26d5da42cb0f..a0b7782f2779 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-langchain/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-langchain/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('langchain instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('langchain instrumentation: included in default integrations (Deno 2.8
Deno.test('langchain instrumentation: orchestrion @langchain/openai:embedQuery channel produces a nested embeddings span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:@langchain/openai:embedQuery');
@@ -43,17 +41,26 @@ Deno.test('langchain instrumentation: orchestrion @langchain/openai:embedQuery c
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.embeddings');
- assertExists(aiSpan, `expected a gen_ai.embeddings child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(aiSpan!.description, 'embeddings text-embedding-3-small');
- assertEquals(aiSpan!.data?.['gen_ai.provider.name'], 'openai');
- assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'embeddings');
- assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'text-embedding-3-small');
- assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.langchain');
+ const aiSpan = children.find(s => getSpanOp(s) === 'gen_ai.embeddings');
+ assertExists(
+ aiSpan,
+ `expected a gen_ai.embeddings child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
+ );
+ assertEquals(aiSpan.name, 'embeddings text-embedding-3-small');
+ assertEquals(aiSpan.attributes['gen_ai.provider.name']?.value, 'openai');
+ assertEquals(aiSpan.attributes['gen_ai.operation.name']?.value, 'embeddings');
+ assertEquals(aiSpan.attributes['gen_ai.request.model']?.value, 'text-embedding-3-small');
+ assertEquals(aiSpan.attributes['sentry.origin']?.value, 'auto.ai.langchain');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts
index e1045407beb5..b10c4f1ed48d 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-langgraph/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('langgraph instrumentation: included in default integrations (Deno 2.8
Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wraps the compiled graph invoke', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:@langchain/langgraph:stateGraphCompile');
@@ -44,19 +42,25 @@ Deno.test('langgraph instrumentation: orchestrion stateGraphCompile channel wrap
await compiledGraph.invoke();
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const invokeAgentSpan = parent.spans?.find(s => s.op === 'gen_ai.invoke_agent');
+ const invokeAgentSpan = children.find(s => getSpanOp(s) === 'gen_ai.invoke_agent');
assertExists(
invokeAgentSpan,
- `expected a gen_ai.invoke_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
+ `expected a gen_ai.invoke_agent child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
);
- assertEquals(invokeAgentSpan!.description, 'invoke_agent my-agent');
- assertEquals(invokeAgentSpan!.data?.['gen_ai.operation.name'], 'invoke_agent');
- assertEquals(invokeAgentSpan!.data?.['gen_ai.agent.name'], 'my-agent');
- assertEquals(invokeAgentSpan!.data?.['sentry.origin'], 'auto.ai.langgraph');
+ assertEquals(invokeAgentSpan.name, 'invoke_agent my-agent');
+ assertEquals(invokeAgentSpan.attributes['gen_ai.operation.name']?.value, 'invoke_agent');
+ assertEquals(invokeAgentSpan.attributes['gen_ai.agent.name']?.value, 'my-agent');
+ assertEquals(invokeAgentSpan.attributes['sentry.origin']?.value, 'auto.ai.langgraph');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-lru-memoizer/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-lru-memoizer/test.ts
index acff0649c8a7..9c2ba2971f11 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-lru-memoizer/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-lru-memoizer/test.ts
@@ -3,16 +3,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { Span } from '@sentry/core';
import type { DenoClient } from '@sentry/deno';
-import { getActiveSpan, init, startSpan, startSpanManual } from '@sentry/deno';
+import { flush, getActiveSpan, init, startSpan, startSpanManual } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('lru-memoizer instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -28,12 +27,11 @@ Deno.test('lru-memoizer instrumentation: included in default integrations (Deno
// new trace; with it, the parent is active again and a span nests under it.
Deno.test('lru-memoizer instrumentation: restores the caller scope onto the memoized callback', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:lru-memoizer:load');
@@ -61,16 +59,19 @@ Deno.test('lru-memoizer instrumentation: restores the caller scope onto the memo
// The callback saw the caller's span restored.
assertEquals(restoredActive, parentSpan);
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
// The span created in the restored callback nested under the caller, not a new trace.
- const child = parent.spans?.find(s => s.description === 'memoized-work');
- assertExists(
- child,
- `expected memoized-work nested under parent, got: ${parent.spans?.map(s => s.description).join(', ')}`,
- );
+ const child = children.find(s => s.name === 'memoized-work');
+ assertExists(child, `expected memoized-work nested under parent, got: ${children.map(s => s.name).join(', ')}`);
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts
index f9693fc07ef9..d7a482dbdaeb 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-mongo/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('mongodb instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('mongodb instrumentation: included in default integrations (Deno 2.8.0
Deno.test('mongodb instrumentation: orchestrion:mongodb:command channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:mongodb:command');
@@ -50,21 +48,27 @@ Deno.test('mongodb instrumentation: orchestrion:mongodb:command channel produces
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const mongoSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(mongoSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(mongoSpan!.description, '{"find":"?","filter":{"name":"?"}}');
- assertEquals(mongoSpan!.data?.['db.system.name'], 'mongodb');
- assertEquals(mongoSpan!.data?.['db.query.text'], '{"find":"?","filter":{"name":"?"}}');
- assertEquals(mongoSpan!.data?.['db.namespace'], 'mydb');
- assertEquals(mongoSpan!.data?.['db.collection.name'], 'users');
- assertEquals(mongoSpan!.data?.['db.operation.name'], 'find');
- assertEquals(mongoSpan!.data?.['server.address'], '127.0.0.1');
- assertEquals(mongoSpan!.data?.['server.port'], 27017);
- assertEquals(mongoSpan!.data?.['sentry.origin'], 'auto.db.mongo');
+ const mongoSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(mongoSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(mongoSpan.name, 'find users');
+ assertEquals(mongoSpan.attributes['db.system.name']?.value, 'mongodb');
+ assertEquals(mongoSpan.attributes['db.query.text']?.value, '{"find":"?","filter":{"name":"?"}}');
+ assertEquals(mongoSpan.attributes['db.namespace']?.value, 'mydb');
+ assertEquals(mongoSpan.attributes['db.collection.name']?.value, 'users');
+ assertEquals(mongoSpan.attributes['db.operation.name']?.value, 'find');
+ assertEquals(mongoSpan.attributes['server.address']?.value, '127.0.0.1');
+ assertEquals(mongoSpan.attributes['server.port']?.value, 27017);
+ assertEquals(mongoSpan.attributes['sentry.origin']?.value, 'auto.db.mongo');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts
index 46854c29b07a..1d3cb1033562 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-mongoose/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('mongoose instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('mongoose instrumentation: included in default integrations (Deno 2.8.
Deno.test('mongoose instrumentation: orchestrion:mongoose:model_save channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:mongoose:model_save');
@@ -51,21 +49,27 @@ Deno.test('mongoose instrumentation: orchestrion:mongoose:model_save channel pro
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const mongooseSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(mongooseSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(mongooseSpan!.description, 'mongoose.BlogPost.save');
- assertEquals(mongooseSpan!.data?.['db.system.name'], 'mongodb');
- assertEquals(mongooseSpan!.data?.['db.namespace'], 'mydb');
- assertEquals(mongooseSpan!.data?.['db.collection.name'], 'blogposts');
- assertEquals(mongooseSpan!.data?.['db.operation.name'], 'save');
- assertEquals(mongooseSpan!.data?.['db.user'], 'root');
- assertEquals(mongooseSpan!.data?.['server.address'], '127.0.0.1');
- assertEquals(mongooseSpan!.data?.['server.port'], 27017);
- assertEquals(mongooseSpan!.data?.['sentry.origin'], 'auto.db.mongoose');
+ const mongooseSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(mongooseSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(mongooseSpan.name, 'save blogposts');
+ assertEquals(mongooseSpan.attributes['db.system.name']?.value, 'mongodb');
+ assertEquals(mongooseSpan.attributes['db.namespace']?.value, 'mydb');
+ assertEquals(mongooseSpan.attributes['db.collection.name']?.value, 'blogposts');
+ assertEquals(mongooseSpan.attributes['db.operation.name']?.value, 'save');
+ assertEquals(mongooseSpan.attributes['db.user']?.value, 'root');
+ assertEquals(mongooseSpan.attributes['server.address']?.value, '127.0.0.1');
+ assertEquals(mongooseSpan.attributes['server.port']?.value, 27017);
+ assertEquals(mongooseSpan.attributes['sentry.origin']?.value, 'auto.db.mongoose');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts
index c263bdace328..6b46f548debb 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-mysql/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('mysql instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -54,12 +53,11 @@ Deno.test('@sentry/deno/import: transforms mysql so it publishes the orchestrion
Deno.test('mysql instrumentation: orchestrion:mysql:query channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:mysql:query');
@@ -84,19 +82,25 @@ Deno.test('mysql instrumentation: orchestrion:mysql:query channel produces a nes
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const mysqlSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(mysqlSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(mysqlSpan!.description, 'SELECT ? AS solution');
- assertEquals(mysqlSpan!.data?.['db.system.name'], 'mysql');
- assertEquals(mysqlSpan!.data?.['db.query.text'], 'SELECT ? AS solution');
- assertEquals(mysqlSpan!.data?.['server.address'], '127.0.0.1');
- assertEquals(mysqlSpan!.data?.['server.port'], 3306);
- assertEquals(mysqlSpan!.data?.['db.user'], 'root');
- assertEquals(mysqlSpan!.data?.['sentry.origin'], 'auto.db.mysql');
+ const mysqlSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(mysqlSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(mysqlSpan.name, 'SELECT');
+ assertEquals(mysqlSpan.attributes['db.system.name']?.value, 'mysql');
+ assertEquals(mysqlSpan.attributes['db.query.text']?.value, 'SELECT ? AS solution');
+ assertEquals(mysqlSpan.attributes['server.address']?.value, '127.0.0.1');
+ assertEquals(mysqlSpan.attributes['server.port']?.value, 3306);
+ assertEquals(mysqlSpan.attributes['db.user']?.value, 'root');
+ assertEquals(mysqlSpan.attributes['sentry.origin']?.value, 'auto.db.mysql');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts
index 1c8efe1364e9..85a992fe89ab 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-mysql2/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('mysql2 instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('mysql2 instrumentation: included in default integrations (Deno 2.8.0+
Deno.test('mysql2 instrumentation: orchestrion:mysql2:query channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:mysql2:query');
@@ -46,20 +44,26 @@ Deno.test('mysql2 instrumentation: orchestrion:mysql2:query channel produces a n
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const mysqlSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(mysqlSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(mysqlSpan!.description, 'SELECT ? AS solution');
- assertEquals(mysqlSpan!.data?.['db.system.name'], 'mysql');
- assertEquals(mysqlSpan!.data?.['db.query.text'], 'SELECT ? AS solution');
- assertEquals(mysqlSpan!.data?.['db.namespace'], 'mydb');
- assertEquals(mysqlSpan!.data?.['db.user'], 'root');
- assertEquals(mysqlSpan!.data?.['server.address'], '127.0.0.1');
- assertEquals(mysqlSpan!.data?.['server.port'], 3306);
- assertEquals(mysqlSpan!.data?.['sentry.origin'], 'auto.db.mysql2');
+ const mysqlSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(mysqlSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(mysqlSpan.name, 'SELECT');
+ assertEquals(mysqlSpan.attributes['db.system.name']?.value, 'mysql');
+ assertEquals(mysqlSpan.attributes['db.query.text']?.value, 'SELECT ? AS solution');
+ assertEquals(mysqlSpan.attributes['db.namespace']?.value, 'mydb');
+ assertEquals(mysqlSpan.attributes['db.user']?.value, 'root');
+ assertEquals(mysqlSpan.attributes['server.address']?.value, '127.0.0.1');
+ assertEquals(mysqlSpan.attributes['server.port']?.value, 3306);
+ assertEquals(mysqlSpan.attributes['sentry.origin']?.value, 'auto.db.mysql2');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts
index 2d1c47b2e30b..98a47dcdf06d 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-openai/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('openai instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('openai instrumentation: included in default integrations (Deno 2.8.0+
Deno.test('openai instrumentation: orchestrion:openai:chat channel produces a nested gen_ai span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:openai:chat');
@@ -46,19 +44,25 @@ Deno.test('openai instrumentation: orchestrion:openai:chat channel produces a ne
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.chat');
- assertExists(aiSpan, `expected a gen_ai.chat child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(aiSpan!.description, 'chat gpt-4o');
- assertEquals(aiSpan!.data?.['gen_ai.provider.name'], 'openai');
- assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'chat');
- assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'gpt-4o');
- assertEquals(aiSpan!.data?.['gen_ai.response.model'], 'gpt-4o-2024-08-06');
- assertEquals(aiSpan!.data?.['gen_ai.usage.total_tokens'], 15);
- assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.ai.openai');
+ const aiSpan = children.find(s => getSpanOp(s) === 'gen_ai.chat');
+ assertExists(aiSpan, `expected a gen_ai.chat child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(aiSpan.name, 'chat gpt-4o');
+ assertEquals(aiSpan.attributes['gen_ai.provider.name']?.value, 'openai');
+ assertEquals(aiSpan.attributes['gen_ai.operation.name']?.value, 'chat');
+ assertEquals(aiSpan.attributes['gen_ai.request.model']?.value, 'gpt-4o');
+ assertEquals(aiSpan.attributes['gen_ai.response.model']?.value, 'gpt-4o-2024-08-06');
+ assertEquals(aiSpan.attributes['gen_ai.usage.total_tokens']?.value, 15);
+ assertEquals(aiSpan.attributes['sentry.origin']?.value, 'auto.ai.openai');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts
index 81012e6fb979..26781f3084c5 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-postgres/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('pg instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -54,12 +53,11 @@ Deno.test('@sentry/deno/import: transforms pg so it publishes the orchestrion ch
Deno.test('pg instrumentation: orchestrion:pg:query channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:pg:query');
@@ -84,19 +82,25 @@ Deno.test('pg instrumentation: orchestrion:pg:query channel produces a nested db
});
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const pgSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(pgSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(pgSpan!.description, 'SELECT ? AS solution');
- assertEquals(pgSpan!.data?.['db.system.name'], 'postgresql');
- assertEquals(pgSpan!.data?.['db.query.text'], 'SELECT ? AS solution');
- assertEquals(pgSpan!.data?.['server.address'], '127.0.0.1');
- assertEquals(pgSpan!.data?.['server.port'], 5432);
- assertEquals(pgSpan!.data?.['db.user'], 'root');
- assertEquals(pgSpan!.data?.['sentry.origin'], 'auto.db.postgres');
+ const pgSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(pgSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(pgSpan.name, 'SELECT');
+ assertEquals(pgSpan.attributes['db.system.name']?.value, 'postgresql');
+ assertEquals(pgSpan.attributes['db.query.text']?.value, 'SELECT ? AS solution');
+ assertEquals(pgSpan.attributes['server.address']?.value, '127.0.0.1');
+ assertEquals(pgSpan.attributes['server.port']?.value, 5432);
+ assertEquals(pgSpan.attributes['db.user']?.value, 'root');
+ assertEquals(pgSpan.attributes['sentry.origin']?.value, 'auto.db.postgres');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-postgresjs/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-postgresjs/test.ts
index 640e51733173..7e3454f2f019 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-postgresjs/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-postgresjs/test.ts
@@ -2,16 +2,15 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('postgres.js instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -21,12 +20,11 @@ Deno.test('postgres.js instrumentation: included in default integrations (Deno 2
Deno.test('postgres.js instrumentation: orchestrion:postgres:handle channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:postgres:handle');
@@ -48,18 +46,24 @@ Deno.test('postgres.js instrumentation: orchestrion:postgres:handle channel prod
query.resolve({ command: 'SELECT' });
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const pgSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(pgSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(pgSpan!.description, 'SELECT name FROM users');
- assertEquals(pgSpan!.data?.['db.system.name'], 'postgres');
- assertEquals(pgSpan!.data?.['db.query.text'], 'SELECT name FROM users');
+ const pgSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(pgSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(pgSpan.name, 'SELECT users');
+ assertEquals(pgSpan.attributes['db.system.name']?.value, 'postgres');
+ assertEquals(pgSpan.attributes['db.query.text']?.value, 'SELECT name FROM users');
// Set by the resolve wrapper from the `command` passed to `query.resolve`.
- assertEquals(pgSpan!.data?.['db.operation.name'], 'SELECT');
- assertEquals(pgSpan!.data?.['sentry.origin'], 'auto.db.postgresjs');
+ assertEquals(pgSpan.attributes['db.operation.name']?.value, 'SELECT');
+ assertEquals(pgSpan.attributes['sentry.origin']?.value, 'auto.db.postgresjs');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts
index 7e9f2d6ec34c..e904576b8248 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-tedious/test.ts
@@ -3,16 +3,15 @@
import { EventEmitter } from 'node:events';
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
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';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('tedious instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
}) as DenoClient;
@@ -22,12 +21,11 @@ Deno.test('tedious instrumentation: included in default integrations (Deno 2.8.0
Deno.test('tedious instrumentation: orchestrion:tedious:execSql channel produces a nested db span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
// The connection and request are `EventEmitter`s; the subscriber only traces
@@ -49,20 +47,26 @@ Deno.test('tedious instrumentation: orchestrion:tedious:execSql channel produces
request.callback();
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const tediousSpan = parent.spans?.find(s => s.op === 'db');
- assertExists(tediousSpan, `expected a db child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`);
- assertEquals(tediousSpan!.description, 'SELECT ?');
- assertEquals(tediousSpan!.data?.['db.system.name'], 'mssql');
- assertEquals(tediousSpan!.data?.['db.namespace'], 'mydb');
- assertEquals(tediousSpan!.data?.['db.user'], 'sa');
- assertEquals(tediousSpan!.data?.['db.query.text'], 'SELECT ?');
- assertEquals(tediousSpan!.data?.['server.address'], '127.0.0.1');
- assertEquals(tediousSpan!.data?.['server.port'], 1433);
- assertEquals(tediousSpan!.data?.['sentry.origin'], 'auto.db.tedious');
+ const tediousSpan = children.find(s => getSpanOp(s) === 'db');
+ assertExists(tediousSpan, `expected a db child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`);
+ assertEquals(tediousSpan.name, 'SELECT');
+ assertEquals(tediousSpan.attributes['db.system.name']?.value, 'mssql');
+ assertEquals(tediousSpan.attributes['db.namespace']?.value, 'mydb');
+ assertEquals(tediousSpan.attributes['db.user']?.value, 'sa');
+ assertEquals(tediousSpan.attributes['db.query.text']?.value, 'SELECT ?');
+ assertEquals(tediousSpan.attributes['server.address']?.value, '127.0.0.1');
+ assertEquals(tediousSpan.attributes['server.port']?.value, 1433);
+ assertEquals(tediousSpan.attributes['sentry.origin']?.value, 'auto.db.tedious');
});
diff --git a/dev-packages/deno-integration-tests/suites/orchestrion-vercel-ai/test.ts b/dev-packages/deno-integration-tests/suites/orchestrion-vercel-ai/test.ts
index 8c7cc42b5fc2..fdd20fd3239d 100644
--- a/dev-packages/deno-integration-tests/suites/orchestrion-vercel-ai/test.ts
+++ b/dev-packages/deno-integration-tests/suites/orchestrion-vercel-ai/test.ts
@@ -2,18 +2,17 @@
import { tracingChannel } from 'node:diagnostics_channel';
import type { DenoClient } from '@sentry/deno';
-import { init, startSpan } from '@sentry/deno';
+import { flush, init, startSpan } from '@sentry/deno';
import { assert } from 'https://deno.land/std@0.212.0/assert/assert.ts';
import { assertExists } from 'https://deno.land/std@0.212.0/assert/assert_exists.ts';
import { assertEquals } from 'https://deno.land/std@0.212.0/assert/assert_equals.ts';
-import { resetGlobals, transactionSink, withTimeout } from '../../src/index.ts';
+import { getSpanOp, resetGlobals, spanSink, withTimeout } from '../../src/index.ts';
Deno.test('vercel-ai instrumentation: included in default integrations (Deno 2.8.0+)', () => {
resetGlobals();
const client = init({
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- traceLifecycle: 'static',
}) as DenoClient;
const names = client.getOptions().integrations.map(i => i.name);
assert(names.includes('VercelAI'), `VercelAI should be in defaults, got ${names.join(', ')}`);
@@ -21,12 +20,11 @@ Deno.test('vercel-ai instrumentation: included in default integrations (Deno 2.8
Deno.test('vercel-ai instrumentation: orchestrion:ai:generateText channel produces a nested invoke_agent span', async () => {
resetGlobals();
- const sink = transactionSink();
+ const sink = spanSink();
init({
- traceLifecycle: 'static',
dsn: 'https://username@domain/123',
tracesSampleRate: 1,
- beforeSendTransaction: sink.beforeSendTransaction,
+ transport: sink.transport,
});
const channel = tracingChannel('orchestrion:ai:generateText');
@@ -45,22 +43,28 @@ Deno.test('vercel-ai instrumentation: orchestrion:ai:generateText channel produc
channel.asyncEnd.publish(ctx);
});
+ await flush();
+
const parent = await withTimeout(
- sink.waitFor(t => t.transaction === 'parent'),
+ sink.waitFor(span => span.is_segment && span.name === 'parent'),
5000,
- "'parent' transaction",
+ "'parent' segment span",
);
+ const children = sink.spans.filter(span => span.parent_span_id === parent.span_id);
+ assertEquals(children.length, 1);
+ assertEquals(children[0]!.trace_id, parent.trace_id);
+ assertEquals(children[0]!.is_segment, false);
- const aiSpan = parent.spans?.find(s => s.op === 'gen_ai.invoke_agent');
+ const aiSpan = children.find(s => getSpanOp(s) === 'gen_ai.invoke_agent');
assertExists(
aiSpan,
- `expected a gen_ai.invoke_agent child span, got ops: ${parent.spans?.map(s => s.op).join(', ')}`,
+ `expected a gen_ai.invoke_agent child span, got ops: ${children.map(s => getSpanOp(s)).join(', ')}`,
);
- assertEquals(aiSpan!.description, 'invoke_agent');
- assertEquals(aiSpan!.data?.['gen_ai.provider.name'], 'openai');
- assertEquals(aiSpan!.data?.['gen_ai.operation.name'], 'invoke_agent');
- assertEquals(aiSpan!.data?.['gen_ai.request.model'], 'gpt-4o');
- assertEquals(aiSpan!.data?.['vercel.ai.operationId'], 'ai.generateText');
- assertEquals(aiSpan!.data?.['gen_ai.usage.total_tokens'], 15);
- assertEquals(aiSpan!.data?.['sentry.origin'], 'auto.vercelai.channel');
+ assertEquals(aiSpan.name, 'invoke_agent');
+ assertEquals(aiSpan.attributes['gen_ai.provider.name']?.value, 'openai');
+ assertEquals(aiSpan.attributes['gen_ai.operation.name']?.value, 'invoke_agent');
+ assertEquals(aiSpan.attributes['gen_ai.request.model']?.value, 'gpt-4o');
+ assertEquals(aiSpan.attributes['vercel.ai.operationId']?.value, 'ai.generateText');
+ assertEquals(aiSpan.attributes['gen_ai.usage.total_tokens']?.value, 15);
+ assertEquals(aiSpan.attributes['sentry.origin']?.value, 'auto.vercelai.channel');
});
diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md
index 4a398a2b9002..cb2964332492 100644
--- a/dev-packages/e2e-tests/README.md
+++ b/dev-packages/e2e-tests/README.md
@@ -116,6 +116,24 @@ Make sure to add a `test:build` and `test:assert` command to the new app's `pack
Sentry packages are automatically resolved to the local build via pnpm overrides injected at test time, so no manual
registry configuration is needed.
+### Test apps that deploy a real Cloudflare Worker
+
+Apps like `cloudflare-workers-send-to-sentry` deploy the built app as a real Worker and check what arrives in Sentry:
+
+- Use `createWorkerGlobalSetup` and `workerGlobalTeardown` from `@sentry-internal/test-utils/cloudflare` as the
+ Playwright global setup and teardown. Tests read the Worker URL from `E2E_TEST_WORKER_URL`, send requests with
+ `fetchFromWorker`, and poll Sentry with the helpers from `@sentry-internal/test-utils/cli` (the app needs `sentry` as
+ a dev dependency).
+- The Worker gets `E2E_TEST_DSN`, `E2E_TEST_WORKER_TOKEN` and the values of the `secrets` option as secrets.
+- CI keeps the Worker after the run, and anyone can reach its `workers.dev` URL. If the Worker can spend money (for
+ example, it holds an LLM API key), it must reject requests without `Authorization: Bearer `.
+ The token is new for every deploy, and `fetchFromWorker` sends it. See
+ `cloudflare-flue-send-to-sentry/src/auth.ts`.
+- Add the `workerPrefix` to `.github/workflows/cleanup-e2e-workers.yml`, which deletes the Worker of a PR when the PR
+ closes.
+- Set `"sentryTest": { "optional": true }` in `package.json`, because only the optional CI job has the Cloudflare
+ secrets.
+
## Troubleshooting
### Common Issues
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/route-provider/[id].astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/route-provider/[id].astro
new file mode 100644
index 000000000000..ff0ee0f13d59
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/route-provider/[id].astro
@@ -0,0 +1,18 @@
+---
+import Layout from '../../layouts/Layout.astro';
+
+export const prerender = false;
+---
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/route-provider.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/route-provider.test.ts
new file mode 100644
index 000000000000..09e444a04c26
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/route-provider.test.ts
@@ -0,0 +1,10 @@
+import { expect, test } from '@playwright/test';
+
+// The route provider reads the route the middleware renders into the page, so this fails if that route
+// never reaches the browser.
+test('resolves the parameterized route through the route provider', async ({ page }) => {
+ await page.goto('/route-provider/123');
+ await page.locator('#resolve-route').click();
+
+ await expect(page.locator('#resolved-route')).toHaveText('/route-provider/[id]');
+});
diff --git a/dev-packages/e2e-tests/test-applications/astro-7-static/package.json b/dev-packages/e2e-tests/test-applications/astro-7-static/package.json
index 35c051bee12d..ce0452bf7e37 100644
--- a/dev-packages/e2e-tests/test-applications/astro-7-static/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-7-static/package.json
@@ -19,7 +19,7 @@
"@playwright/test": "~1.63.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
- "astro": "^7.2.4",
+ "astro": "^7.2.8",
"ioredis": "5.10.1",
"mysql": "^2.18.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/astro-7/package.json b/dev-packages/e2e-tests/test-applications/astro-7/package.json
index acd09a130916..33126242d1f6 100644
--- a/dev-packages/e2e-tests/test-applications/astro-7/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-7/package.json
@@ -19,7 +19,7 @@
"@playwright/test": "~1.63.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/astro": "file:../../packed/sentry-astro-packed.tgz",
- "astro": "^7.2.4",
+ "astro": "^7.2.8",
"ioredis": "5.10.1",
"mysql": "^2.18.1"
},
diff --git a/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts b/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts
index 31e6c83eabbe..2a157ff659c6 100644
--- a/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts
+++ b/dev-packages/e2e-tests/test-applications/browser-bfcache/tests/bfcache.test.ts
@@ -277,6 +277,33 @@ test('a hit carries the parameterized route that was on the scope before the fre
expect(attr(hit, 'sentry.segment.name')).toBe('/users/:id');
});
+// A registered route provider is preferred over the scope, which is what lets a framework SDK name the
+// segment even when nothing stamped a route on the scope. The scope holds a different name here to prove it.
+test('a hit carries the route resolved by a registered route provider', async ({ page }) => {
+ const hitPromise = waitForMetric(PROXY_SERVER_NAME, metric => isNavigation(metric, 'hit'));
+
+ await page.goto('/');
+ await page.waitForFunction(() => document.title === 'BFCache E2E - Page 1');
+
+ await page.evaluate(() => {
+ const { Sentry } = window as unknown as { Sentry: typeof import('@sentry/browser') };
+ Sentry.getCurrentScope().setTransactionName('/from-scope');
+ Sentry.setRouteProvider({ resolveRoute: () => '/users/:id', resolveCurrentRoute: () => '/users/:id' });
+ });
+
+ await page.click('#to-page-2');
+ await page.waitForFunction(() => document.title === 'BFCache E2E - Page 2');
+ await page.waitForTimeout(500);
+
+ await page.evaluate(() => history.back());
+ await page.waitForFunction(() => (window as unknown as { __bfcacheRestored?: boolean }).__bfcacheRestored === true, {
+ timeout: 5000,
+ });
+
+ const hit = await hitPromise;
+ expect(attr(hit, 'sentry.segment.name')).toBe('/users/:id');
+});
+
// Without a routing integration the scope has no transaction name, so the segment name falls back to
// `location.pathname` (page 1 is served at '/'). This matches how browserTracing names an unrouted pageload.
test('a hit falls back to the raw pathname when no route is on the scope', async ({ page }) => {
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/.gitignore
new file mode 100644
index 000000000000..b6c344e957e1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/.gitignore
@@ -0,0 +1,5 @@
+dist
+.wrangler
+node_modules
+test-results
+pnpm-lock.yaml
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/flue.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/flue.config.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/flue.config.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/flue.config.ts
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/global-setup.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/global-setup.ts
new file mode 100644
index 000000000000..631329e9f0e3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/global-setup.ts
@@ -0,0 +1,6 @@
+import { createWorkerGlobalSetup } from '@sentry-internal/test-utils/cloudflare';
+
+export default createWorkerGlobalSetup({
+ workerPrefix: 'e2e-flue-send-to-sentry',
+ secrets: { OPENROUTER_API_KEY: 'E2E_OPENROUTER_API_KEY' },
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/global-teardown.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/global-teardown.ts
new file mode 100644
index 000000000000..c7bcfe696e66
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/global-teardown.ts
@@ -0,0 +1 @@
+export { workerGlobalTeardown as default } from '@sentry-internal/test-utils/cloudflare';
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/package.json
similarity index 64%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/package.json
index ec82ef912a33..7f7be894c7e6 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/package.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/package.json
@@ -1,16 +1,15 @@
{
- "name": "cloudflare-flue",
+ "name": "cloudflare-flue-send-to-sentry",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
- "preview": "vite preview --port 4112",
- "typecheck": "tsc --noEmit",
- "clean": "npx rimraf node_modules dist .wrangler pnpm-lock.yaml",
- "test:build": "pnpm install && OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY pnpm build",
- "test:assert": "pnpm test:prod",
- "test:prod": "TEST_ENV=production OPENROUTER_API_KEY=$E2E_OPENROUTER_API_KEY playwright test"
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml dist .wrangler",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm typecheck && pnpm test"
},
"dependencies": {
"@flue/runtime": "2.0.5",
@@ -27,9 +26,11 @@
"@flue/vite": "2.0.5",
"@playwright/test": "~1.63.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
+ "@types/node": "^24.0.0",
+ "sentry": "~0.44.1",
"typescript": "^5.5.2",
"vite": "7.3.5",
- "wrangler": "^4.86.0"
+ "wrangler": "^4.114.0"
},
"volta": {
"node": "24.15.0",
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/playwright.config.ts
new file mode 100644
index 000000000000..cfb1b52d440e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/playwright.config.ts
@@ -0,0 +1,16 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig(undefined, {
+ // The tests read what arrived in Sentry through the sentry CLI, so there is no event proxy to start.
+ webServer: undefined,
+ // The worker is deployed once for the whole run and deleted again afterwards.
+ globalSetup: './global-setup.ts',
+ globalTeardown: './global-teardown.ts',
+ // Each test drives a real OpenRouter turn and then waits ~2min until its spans are queryable.
+ timeout: 300_000,
+ // Every test spends most of its time polling Sentry, so run them all at once.
+ fullyParallel: true,
+ workers: '100%',
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/agents/hello.ts
similarity index 95%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/agents/hello.ts
index df0589a1988e..4671208793f3 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/agents/hello.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/agents/hello.ts
@@ -17,9 +17,9 @@ export function Hello() {
input: v.object({ city: v.string() }),
// Wrapped in a manual span: Flue runs the tool while the SDK's `execute_tool` span is active,
// so this should nest directly under it rather than landing beside it.
- run: ({ city }) =>
+ run: ({ data }) =>
Sentry.startSpan({ name: 'resolve-weather', attributes: { 'weather.source': 'static-table' } }, () => {
- return `It is 21 degrees and sunny in ${city}.`;
+ return `It is 21 degrees and sunny in ${data.city}.`;
}),
});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/app.ts
similarity index 53%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/app.ts
index 613e9453c506..974c275b185e 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/app.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/app.ts
@@ -1,9 +1,13 @@
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { Hello } from './agents/hello.ts';
+import { requireTestToken } from './auth.ts';
const app = new Hono();
+// global-setup.ts waits for this route to answer before the tests start.
+app.get('/', c => c.text('Hello World!'));
+app.use('/agents/*', requireTestToken);
app.route('/agents/hello', createAgentRouter(Hello));
export default app;
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/auth.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/auth.ts
new file mode 100644
index 000000000000..a2c81dd494f1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/auth.ts
@@ -0,0 +1,14 @@
+import { createMiddleware } from 'hono/factory';
+
+/**
+ * CI keeps the Worker after the run, and it holds an OpenRouter key. So the agent routes only answer
+ * the test run that deployed the Worker. `createWorkerGlobalSetup` makes the token for each deploy, and
+ * `fetchFromWorker` sends it.
+ */
+export const requireTestToken = createMiddleware<{ Bindings: Env }>(async (c, next) => {
+ const token = c.env.E2E_TEST_WORKER_TOKEN;
+ if (!token || c.req.header('Authorization') !== `Bearer ${token}`) {
+ return c.text('Unauthorized', 401);
+ }
+ await next();
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/cloudflare.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/cloudflare.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/src/cloudflare.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/cloudflare.ts
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/env.d.ts
similarity index 68%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/env.d.ts
index ecd15b570543..2bbac1d75aa7 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/env.d.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/env.d.ts
@@ -1,4 +1,5 @@
interface Env {
E2E_TEST_DSN: string;
OPENROUTER_API_KEY: string;
+ E2E_TEST_WORKER_TOKEN: string;
}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/loaders.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/loaders.ts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/src/loaders.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/loaders.ts
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/sentry.ts
similarity index 84%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/sentry.ts
index 5cbc2856bfff..c983d58861ab 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/src/sentry.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/src/sentry.ts
@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/cloudflare';
// Each Flue agent runs in its own Durable Object, so the DO class is what has to be wrapped for
// `Sentry.init` to run and spans to be flushed. The agent module re-exports this as `cloudflare`,
-// which is how Flue picks it up — defining it here alone does nothing.
+// which is how Flue picks it up, defining it here alone does nothing.
//
// There is deliberately no `instrument()` call in this app: registering the Flue instrumentation is
// what `@sentry/cloudflare/vite` does at build time, and these tests exist to prove it.
@@ -13,7 +13,6 @@ export const cloudflare = extend({
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
environment: 'qa',
- tunnel: 'http://localhost:3031/', // proxy server
tracesSampleRate: 1.0,
}),
Final,
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/dataloader.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/dataloader.test.ts
new file mode 100644
index 000000000000..defcd51510d4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/dataloader.test.ts
@@ -0,0 +1,47 @@
+import { expect, test } from '@playwright/test';
+import type { TraceItem } from '@sentry-internal/test-utils/cli';
+import {
+ EVENT_POLLING_OPTIONS,
+ fetchSpanAttributes,
+ fetchTrace,
+ findTraceIdOfSpan,
+ flattenTrace,
+ traceTarget,
+} from '@sentry-internal/test-utils/cli';
+import { newInstanceId, runAgentTurn } from './utils';
+
+// Set by global-setup.ts once the worker for this run is deployed.
+const workerUrl = process.env.E2E_TEST_WORKER_URL!;
+
+/**
+ * On Cloudflare orchestrion runs at build time (`@sentry/cloudflare/vite` injects the channels),
+ * so unlike the Node app there is no `--import` bootstrap and no variant: the span is either there
+ * or the build-time instrumentation regressed.
+ */
+test('captures orchestrion-instrumented dataloader spans in the same trace as the AI spans', async () => {
+ const conversationId = await runAgentTurn(
+ workerUrl,
+ newInstanceId('dataloader'),
+ 'Please call count_items to count the items.',
+ );
+
+ let traceId: string | undefined;
+ await expect
+ .poll(() => (traceId = findTraceIdOfSpan(`gen_ai.conversation.id:${conversationId}`)), EVENT_POLLING_OPTIONS)
+ .toBeDefined();
+
+ console.log(`Polling for the dataloader span: sentry trace view ${traceTarget(traceId!)}`);
+
+ let spans: TraceItem[] = [];
+ await expect
+ .poll(() => (spans = flattenTrace(fetchTrace(traceId!))).map(span => span.op), EVENT_POLLING_OPTIONS)
+ .toEqual(expect.arrayContaining(['gen_ai.execute_tool', 'cache.get']));
+
+ const dataloaderSpan = spans.find(span => span.op === 'cache.get');
+ const toolSpan = spans.find(span => span.description === 'execute_tool count_items');
+
+ await expect
+ .poll(() => fetchSpanAttributes(traceId!, dataloaderSpan!.event_id!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({ origin: 'auto.db.dataloader' });
+ expect(toolSpan).toBeDefined();
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/errors.test.ts
new file mode 100644
index 000000000000..6b7258deed4d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/errors.test.ts
@@ -0,0 +1,52 @@
+import { expect, test } from '@playwright/test';
+import type { TraceItem } from '@sentry-internal/test-utils/cli';
+import {
+ EVENT_POLLING_OPTIONS,
+ fetchEvent,
+ fetchSpanAttributes,
+ fetchTrace,
+ findErrorInTrace,
+ findTraceIdOfSpan,
+ flattenTrace,
+ traceTarget,
+} from '@sentry-internal/test-utils/cli';
+import { newInstanceId, runAgentTurn } from './utils';
+
+// Set by global-setup.ts once the worker for this run is deployed.
+const workerUrl = process.env.E2E_TEST_WORKER_URL!;
+
+test('captures an error thrown inside a Flue tool and marks its span errored', async () => {
+ const conversationId = await runAgentTurn(
+ workerUrl,
+ newInstanceId('failure'),
+ 'Please call fail_now to trigger a failure.',
+ );
+
+ let traceId: string | undefined;
+ await expect
+ .poll(() => (traceId = findTraceIdOfSpan(`gen_ai.conversation.id:${conversationId}`)), EVENT_POLLING_OPTIONS)
+ .toBeDefined();
+
+ console.log(`Polling for the tool error: sentry trace view ${traceTarget(traceId!)}`);
+
+ let error: TraceItem | undefined;
+ await expect.poll(() => (error = findErrorInTrace(traceId!)), EVENT_POLLING_OPTIONS).toBeDefined();
+ await expect
+ .poll(
+ () =>
+ fetchEvent(error!.event_id!)?.entries.find(entry => entry.type === 'exception')?.data.values?.[0]?.mechanism
+ ?.type,
+ EVENT_POLLING_OPTIONS,
+ )
+ .toBe('auto.ai.flue');
+
+ let spans: TraceItem[] = [];
+ await expect
+ .poll(() => (spans = flattenTrace(fetchTrace(traceId!))).map(span => span.description), EVENT_POLLING_OPTIONS)
+ .toContain('execute_tool fail_now');
+
+ const executeTool = spans.find(span => span.description === 'execute_tool fail_now');
+ await expect
+ .poll(() => fetchSpanAttributes(traceId!, executeTool!.event_id!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({ 'gen_ai.tool.name': 'fail_now', 'span.status': 'error' });
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/flue.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/flue.test.ts
new file mode 100644
index 000000000000..ac6c0dab180f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/flue.test.ts
@@ -0,0 +1,86 @@
+import { expect, test } from '@playwright/test';
+import type { TraceItem } from '@sentry-internal/test-utils/cli';
+import {
+ EVENT_POLLING_OPTIONS,
+ fetchSpanAttributes,
+ fetchTrace,
+ findTraceIdOfSpan,
+ flattenTrace,
+ traceTarget,
+} from '@sentry-internal/test-utils/cli';
+import { newInstanceId, runAgentTurn } from './utils';
+
+// Set by global-setup.ts once the worker for this run is deployed.
+const workerUrl = process.env.E2E_TEST_WORKER_URL!;
+
+/**
+ * This app never calls `instrument()`. On Cloudflare the registration comes from the build:
+ * `@sentry/cloudflare/vite` provides the `@flue/runtime` binding and the orchestrion registration
+ * installs `flueIntegration()`. So any `gen_ai` span here is itself the proof that the auto-wiring
+ * worked, and a manual-registration regression shows up as an empty trace, not a wrong attribute.
+ */
+test('instruments a Flue agent with no manual instrument() call', async () => {
+ const conversationId = await runAgentTurn(workerUrl, newInstanceId('weather'), 'What is the weather in Paris?');
+
+ let traceId: string | undefined;
+ await expect
+ .poll(() => (traceId = findTraceIdOfSpan(`gen_ai.conversation.id:${conversationId}`)), EVENT_POLLING_OPTIONS)
+ .toBeDefined();
+
+ console.log(`Polling for the agent spans: sentry trace view ${traceTarget(traceId!)}`);
+
+ let spans: TraceItem[] = [];
+ await expect
+ .poll(() => (spans = flattenTrace(fetchTrace(traceId!))).map(span => span.op), EVENT_POLLING_OPTIONS)
+ .toEqual(expect.arrayContaining(['gen_ai.invoke_agent', 'gen_ai.chat', 'gen_ai.execute_tool']));
+
+ const invokeAgent = spans.find(span => span.op === 'gen_ai.invoke_agent');
+ const chat = spans.find(span => span.op === 'gen_ai.chat');
+ const executeTool = spans.find(span => span.op === 'gen_ai.execute_tool');
+
+ await expect
+ .poll(() => fetchSpanAttributes(traceId!, invokeAgent!.event_id!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({
+ origin: 'auto.ai.flue',
+ 'gen_ai.agent.name': 'Hello',
+ });
+
+ await expect
+ .poll(() => fetchSpanAttributes(traceId!, chat!.event_id!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({
+ origin: 'auto.ai.flue',
+ 'gen_ai.provider.name': 'openrouter',
+ 'gen_ai.usage.input_tokens': expect.anything(),
+ 'gen_ai.cost.total_tokens': expect.anything(),
+ });
+
+ await expect
+ .poll(() => fetchSpanAttributes(traceId!, executeTool!.event_id!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({ 'gen_ai.tool.name': 'get_weather' });
+ expect(chat?.parent_span_id).toBe(invokeAgent?.event_id);
+ expect(executeTool?.parent_span_id).toBe(invokeAgent?.event_id);
+});
+
+test('nests a manual span raised inside a tool under that tool span', async () => {
+ const conversationId = await runAgentTurn(workerUrl, newInstanceId('manual-span'), 'What is the weather in Berlin?');
+
+ let traceId: string | undefined;
+ await expect
+ .poll(() => (traceId = findTraceIdOfSpan(`gen_ai.conversation.id:${conversationId}`)), EVENT_POLLING_OPTIONS)
+ .toBeDefined();
+
+ console.log(`Polling for the manual span: sentry trace view ${traceTarget(traceId!)}`);
+
+ let spans: TraceItem[] = [];
+ await expect
+ .poll(() => (spans = flattenTrace(fetchTrace(traceId!))).map(span => span.description), EVENT_POLLING_OPTIONS)
+ .toEqual(expect.arrayContaining(['execute_tool get_weather', 'resolve-weather']));
+
+ const executeTool = spans.find(span => span.op === 'gen_ai.execute_tool');
+ const manualSpan = spans.find(span => span.description === 'resolve-weather');
+
+ await expect
+ .poll(() => fetchSpanAttributes(traceId!, manualSpan!.event_id!), EVENT_POLLING_OPTIONS)
+ .toMatchObject({ 'weather.source': 'static-table' });
+ expect(manualSpan?.parent_span_id).toBe(executeTool?.event_id);
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/utils.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/utils.ts
new file mode 100644
index 000000000000..1730d7781025
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tests/utils.ts
@@ -0,0 +1,38 @@
+import { fetchFromWorker } from '@sentry-internal/test-utils/cloudflare';
+
+/** An agent instance id nothing has used yet, so a settled record cannot end the wait early. */
+export function newInstanceId(prefix: string): string {
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+}
+
+/**
+ * Run one agent turn and wait for it to settle. Returns the id of the conversation, which the SDK
+ * sets as `gen_ai.conversation.id` on the agent span. Flue runs the turn from a Durable Object alarm,
+ * and the SDK starts a new trace for every alarm, so the tests find the trace by this id.
+ *
+ * `POST /:id` only admits the work (it returns `202` and the turn runs after), so this reads the
+ * conversation back until it reports a settlement.
+ */
+export async function runAgentTurn(workerUrl: string, instanceId: string, message: string): Promise {
+ const url = `${workerUrl}/agents/hello/${instanceId}`;
+
+ await fetchFromWorker(url, 202, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ kind: 'user', body: message }),
+ });
+
+ const deadline = Date.now() + 90_000;
+ while (Date.now() < deadline) {
+ const conversation = JSON.parse(await fetchFromWorker(url, 200)) as {
+ conversationId: string;
+ settlements?: unknown[];
+ };
+ if (conversation.settlements?.length) {
+ return conversation.conversationId;
+ }
+ await new Promise(resolve => setTimeout(resolve, 1_000));
+ }
+
+ throw new Error(`Flue turn for "${instanceId}" did not settle within 90s`);
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tsconfig.json
similarity index 92%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tsconfig.json
index c612b8a5194a..83c877306e74 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tsconfig.json
@@ -4,6 +4,7 @@
"lib": ["es2021"],
"module": "es2022",
"moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
"resolveJsonModule": true,
"allowJs": true,
"checkJs": false,
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tsconfig.node.json
new file mode 100644
index 000000000000..cd031756584c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/tsconfig.node.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "types": ["node"]
+ },
+ "exclude": [],
+ "include": ["tests/**/*", "global-setup.ts", "global-teardown.ts", "playwright.config.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/vite.config.mts b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/vite.config.mts
similarity index 100%
rename from dev-packages/e2e-tests/test-applications/cloudflare-flue/vite.config.mts
rename to dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/vite.config.mts
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/wrangler.jsonc
new file mode 100644
index 000000000000..f3883b2d5ff9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-flue-send-to-sentry/wrangler.jsonc
@@ -0,0 +1,12 @@
+{
+ "$schema": "./node_modules/wrangler/config-schema.json",
+ // Placeholder only: every test run deploys under a unique name, see global-setup.ts.
+ "name": "cloudflare-flue-send-to-sentry",
+ "compatibility_date": "2026-06-01",
+ "compatibility_flags": ["nodejs_compat"],
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FlueHelloAgent"] }],
+ "workers_dev": true,
+ // Workers Logs keep the invocations of the last 7 days, so a failed CI run can still be inspected.
+ "observability": { "enabled": true },
+ "secrets": { "required": ["E2E_TEST_DSN", "OPENROUTER_API_KEY", "E2E_TEST_WORKER_TOKEN"] },
+}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts
deleted file mode 100644
index a11f1656965f..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/playwright.config.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { getPlaywrightConfig } from '@sentry-internal/test-utils';
-
-const testEnv = process.env.TEST_ENV;
-
-if (!testEnv) {
- throw new Error('No test env defined');
-}
-
-const config = getPlaywrightConfig(
- { startCommand: 'pnpm preview', port: 4112 },
- // Each test drives a real OpenRouter turn and then waits for the spans to flush, which does not
- // fit the default 30s timeout when the provider is slow.
- { timeout: 90_000 },
-);
-
-export default config;
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs
deleted file mode 100644
index a24f2d9a08a0..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/start-event-proxy.mjs
+++ /dev/null
@@ -1,6 +0,0 @@
-import { startEventProxyServer } from '@sentry-internal/test-utils';
-
-startEventProxyServer({
- port: 3031,
- proxyServerName: 'cloudflare-flue',
-});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts
deleted file mode 100644
index 0193e0febbd0..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/dataloader.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
-import { newConversationId, runAgentTurn } from './utils';
-
-const APP = 'cloudflare-flue';
-
-const isDataloaderSpan = (span: { attributes?: Record }): boolean =>
- span.attributes?.['sentry.origin']?.value === 'auto.db.dataloader';
-
-/**
- * On Cloudflare orchestrion runs at build time — `@sentry/cloudflare/vite` injects the channels —
- * so unlike the Node app there is no `--import` bootstrap and no variant: the span is either there
- * or the build-time instrumentation regressed.
- */
-test('captures orchestrion-instrumented dataloader spans in the same trace as the AI spans', async ({ baseURL }) => {
- const spansPromise = collectStreamedSpans(
- APP,
- spansOfTrace =>
- spansOfTrace.some(span => span.attributes?.['gen_ai.tool.name']?.value === 'count_items') &&
- spansOfTrace.some(isDataloaderSpan),
- );
-
- await runAgentTurn(baseURL!, newConversationId('dataloader'), 'Please call count_items to count the items.');
-
- const spans = await spansPromise;
- const dataloaderSpan = spans.find(isDataloaderSpan);
- const toolSpan = spans.find(span => span.attributes?.['gen_ai.tool.name']?.value === 'count_items');
-
- expect(getSpanOp(dataloaderSpan!)).toBe('cache.get');
- expect(dataloaderSpan?.trace_id).toBe(toolSpan?.trace_id);
-});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts
deleted file mode 100644
index 98a12ebc4c3a..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/errors.test.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpans, waitForError } from '@sentry-internal/test-utils';
-import { newConversationId, runAgentTurn } from './utils';
-
-const APP = 'cloudflare-flue';
-
-test('captures an error thrown inside a Flue tool and marks its span errored', async ({ baseURL }) => {
- const errorPromise = waitForError(
- APP,
- event => event.exception?.values?.[0]?.value === 'Intentional flue tool failure',
- );
- const spansPromise = collectStreamedSpans(APP, spansOfTrace =>
- spansOfTrace.some(span => span.attributes?.['gen_ai.tool.name']?.value === 'fail_now'),
- );
-
- await runAgentTurn(baseURL!, newConversationId('failure'), 'Please call fail_now to trigger a failure.');
-
- const error = await errorPromise;
- expect(error.exception?.values?.[0]?.mechanism?.type).toBe('auto.ai.flue');
-
- const spans = await spansPromise;
- const executeTool = spans.find(span => span.attributes?.['gen_ai.tool.name']?.value === 'fail_now');
- expect(executeTool?.status).toBe('error');
-});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts
deleted file mode 100644
index 41142178aebd..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/flue.test.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { expect, test } from '@playwright/test';
-import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
-import { newConversationId, runAgentTurn } from './utils';
-
-const APP = 'cloudflare-flue';
-
-type SpanLike = { name?: string; attributes?: Record };
-
-const usedTool = (toolName: string) => (spansOfTrace: SpanLike[]) =>
- spansOfTrace.some(span => span.attributes?.['gen_ai.tool.name']?.value === toolName);
-
-/**
- * This app never calls `instrument()`. On Cloudflare the registration comes from the build:
- * `@sentry/cloudflare/vite` provides the `@flue/runtime` binding and the orchestrion registration
- * installs `flueIntegration()`. So any `gen_ai` span here is itself the proof that the auto-wiring
- * worked — a manual-registration regression shows up as an empty trace, not a wrong attribute.
- */
-test('instruments a Flue agent with no manual instrument() call', async ({ baseURL }) => {
- const spansPromise = collectStreamedSpans(
- APP,
- spansOfTrace =>
- spansOfTrace.some(span => getSpanOp(span) === 'gen_ai.invoke_agent') && usedTool('get_weather')(spansOfTrace),
- );
-
- await runAgentTurn(baseURL!, newConversationId('weather'), 'What is the weather in Paris?');
-
- const spans = await spansPromise;
- const invokeAgent = spans.find(span => getSpanOp(span) === 'gen_ai.invoke_agent');
- const chat = spans.find(span => getSpanOp(span) === 'gen_ai.chat');
- const executeTool = spans.find(span => getSpanOp(span) === 'gen_ai.execute_tool');
-
- expect(invokeAgent?.attributes?.['sentry.origin']?.value).toBe('auto.ai.flue');
- expect(invokeAgent?.attributes?.['gen_ai.agent.name']?.value).toBe('Hello');
-
- expect(chat?.attributes?.['sentry.origin']?.value).toBe('auto.ai.flue');
- expect(chat?.attributes?.['gen_ai.provider.name']?.value).toBe('openrouter');
- expect(typeof chat?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe('number');
- expect(typeof chat?.attributes?.['gen_ai.cost.total_tokens']?.value).toBe('number');
-
- expect(executeTool?.attributes?.['gen_ai.tool.name']?.value).toBe('get_weather');
- expect(chat?.parent_span_id).toBe(invokeAgent?.span_id);
- expect(executeTool?.parent_span_id).toBe(invokeAgent?.span_id);
-});
-
-test('nests a manual span raised inside a tool under that tool span', async ({ baseURL }) => {
- const spansPromise = collectStreamedSpans(
- APP,
- spansOfTrace => usedTool('get_weather')(spansOfTrace) && spansOfTrace.some(span => span.name === 'resolve-weather'),
- );
-
- await runAgentTurn(baseURL!, newConversationId('manual-span'), 'What is the weather in Berlin?');
-
- const spans = await spansPromise;
- const executeTool = spans.find(span => getSpanOp(span) === 'gen_ai.execute_tool');
- const manualSpan = spans.find(span => span.name === 'resolve-weather');
-
- expect(manualSpan?.attributes?.['weather.source']?.value).toBe('static-table');
- expect(manualSpan?.parent_span_id).toBe(executeTool?.span_id);
-});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts b/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts
deleted file mode 100644
index 4a3932ada389..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/tests/utils.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { expect } from '@playwright/test';
-
-/** A conversation id nothing has used yet, so a settled record cannot end the wait early. */
-export function newConversationId(prefix: string): string {
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
-}
-
-/**
- * Run one agent turn and wait for it to settle.
- *
- * `POST /:id` only admits the work — it returns `202` and the turn runs after — so this reads the
- * conversation back until it reports a settlement.
- */
-export async function runAgentTurn(baseURL: string, conversationId: string, message: string): Promise {
- const url = `${baseURL}/agents/hello/${conversationId}`;
-
- const res = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ kind: 'user', body: message }),
- });
- expect(res.status).toBe(202);
- await res.text();
-
- const deadline = Date.now() + 60_000;
- while (Date.now() < deadline) {
- const conversation = (await (await fetch(url)).json()) as { settlements?: unknown[] };
- if (conversation.settlements?.length) {
- return;
- }
- await new Promise(resolve => setTimeout(resolve, 250));
- }
-
- throw new Error(`Flue turn for "${conversationId}" did not settle within 60s`);
-}
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc
deleted file mode 100644
index 273779ec6976..000000000000
--- a/dev-packages/e2e-tests/test-applications/cloudflare-flue/wrangler.jsonc
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "$schema": "./node_modules/wrangler/config-schema.json",
- "name": "cloudflare-flue",
- "compatibility_date": "2026-06-01",
- "compatibility_flags": ["nodejs_compat"],
- "migrations": [{ "tag": "v1", "new_sqlite_classes": ["FlueHelloAgent"] }],
- // `vite build` snapshots these from the build-time environment into `dist/.dev.vars`, and
- // `vite preview` reads that file in preference to its own environment. So both have to be set
- // for the build, which is why `test:build` maps `E2E_OPENROUTER_API_KEY` across.
- "secrets": { "required": ["E2E_TEST_DSN", "OPENROUTER_API_KEY"] },
-}
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 324c9161cf69..87ae6c9cff41 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
@@ -31,6 +31,13 @@ export default {
await env.AI.run('@cf/meta/llama-3.2-1b-instruct', { prompt: 'Say hi', max_tokens: 5 });
return Response.json({ traceId: spanContext?.traceId });
}
+ case '/test-workers-ai-jev': {
+ await env.AI.run('typesafe/jev', {
+ state: 'Help! My payouts have been failing for 3 days.',
+ questions: { is_urgent: { type: 'noul', instructions: 'Does this convey urgency?' } },
+ });
+ 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 2397db37089f..b391f2f8244b 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
@@ -93,3 +93,24 @@ test('Sends a Workers AI gen_ai span to Sentry', async () => {
'gen_ai.output.messages': expect.stringContaining('"role":"assistant"'),
});
});
+
+test('Sends a Workers AI gen_ai.evaluate span for a TypeSafe Jev call to Sentry', async () => {
+ const { traceId }: { traceId: string } = JSON.parse(await fetchFromWorker(`${workerUrl}/test-workers-ai-jev`, 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': 'typesafe/jev',
+ 'gen_ai.response.model': expect.stringMatching(/^jev-/),
+ 'gen_ai.input.messages': expect.stringContaining('My payouts have been failing'),
+ 'gen_ai.output.messages': expect.stringContaining('"is_urgent"'),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts
index 8d53746ee43d..81c4f72bfdda 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers-static/tests/index.test.ts
@@ -41,7 +41,7 @@ test("Request processed by DurableObject's fetch is recorded", async ({ baseURL
test('Websocket.webSocketMessage', async ({ baseURL }) => {
const eventWaiter = waitForError('cloudflare-workers-static', event => {
- return !!event.exception?.values?.[0];
+ return event.exception?.values?.[0]?.value === 'Should be recorded in Sentry: webSocketMessage';
});
const url = new URL('/pass-to-object/ws', baseURL);
url.protocol = url.protocol.replace('http', 'ws');
@@ -57,7 +57,7 @@ test('Websocket.webSocketMessage', async ({ baseURL }) => {
test('Websocket.webSocketClose', async ({ baseURL }) => {
const eventWaiter = waitForError('cloudflare-workers-static', event => {
- return !!event.exception?.values?.[0];
+ return event.exception?.values?.[0]?.value === 'Should be recorded in Sentry: webSocketClose';
});
const url = new URL('/pass-to-object/ws', baseURL);
url.protocol = url.protocol.replace('http', 'ws');
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts
index 3a5e72530801..ca79ad3c71ec 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts
@@ -63,7 +63,7 @@ test("Request processed by DurableObject's fetch is recorded", async ({ baseURL
test('Websocket.webSocketMessage', async ({ baseURL }) => {
const eventWaiter = waitForError('cloudflare-workers', event => {
- return !!event.exception?.values?.[0];
+ return event.exception?.values?.[0]?.value === 'Should be recorded in Sentry: webSocketMessage';
});
const url = new URL('/pass-to-object/ws', baseURL);
url.protocol = url.protocol.replace('http', 'ws');
@@ -79,7 +79,7 @@ test('Websocket.webSocketMessage', async ({ baseURL }) => {
test('Websocket.webSocketClose', async ({ baseURL }) => {
const eventWaiter = waitForError('cloudflare-workers', event => {
- return !!event.exception?.values?.[0];
+ return event.exception?.values?.[0]?.value === 'Should be recorded in Sentry: webSocketClose';
});
const url = new URL('/pass-to-object/ws', baseURL);
url.protocol = url.protocol.replace('http', 'ws');
diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/tests/index.test.ts
index b815e7b7e3c7..b24d5f804ddd 100644
--- a/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/tests/index.test.ts
+++ b/dev-packages/e2e-tests/test-applications/cloudflare-workersentrypoint/tests/index.test.ts
@@ -63,7 +63,7 @@ test("Request processed by DurableObject's fetch is recorded", async ({ baseURL
test('Websocket.webSocketMessage', async ({ baseURL }) => {
const eventWaiter = waitForError('cloudflare-workersentrypoint', event => {
- return !!event.exception?.values?.[0];
+ return event.exception?.values?.[0]?.value === 'Should be recorded in Sentry: webSocketMessage';
});
const url = new URL('/pass-to-object/ws', baseURL);
url.protocol = url.protocol.replace('http', 'ws');
@@ -79,7 +79,7 @@ test('Websocket.webSocketMessage', async ({ baseURL }) => {
test('Websocket.webSocketClose', async ({ baseURL }) => {
const eventWaiter = waitForError('cloudflare-workersentrypoint', event => {
- return !!event.exception?.values?.[0];
+ return event.exception?.values?.[0]?.value === 'Should be recorded in Sentry: webSocketClose';
});
const url = new URL('/pass-to-object/ws', baseURL);
url.protocol = url.protocol.replace('http', 'ws');
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx
new file mode 100644
index 000000000000..6134dbb17ab0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/app/routes/route-provider.$id.tsx
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/remix';
+import { useEffect, useState } from 'react';
+
+export default function RouteProvider() {
+ const [route, setRoute] = useState();
+
+ useEffect(() => {
+ setRoute(Sentry.resolveCurrentRoute() ?? 'unresolved');
+ }, []);
+
+ return
{route}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts
new file mode 100644
index 000000000000..a99e7ec07314
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express/tests/route-provider.test.ts
@@ -0,0 +1,9 @@
+import { expect, test } from '@playwright/test';
+
+// The route provider is backed by the manifest the Vite plugin injects into the client bundle, so this
+// fails if that manifest never reaches the browser.
+test('resolves the parameterized route through the route provider', async ({ page }) => {
+ await page.goto('/route-provider/123');
+
+ await expect(page.locator('#resolved-route')).toHaveText('/route-provider/:id');
+});
diff --git a/dev-packages/e2e-tests/test-applications/deno/tests/streamed/transactions.test.ts b/dev-packages/e2e-tests/test-applications/deno/tests/streamed/transactions.test.ts
index cb9bb624f6bd..83bf22f3d6dc 100644
--- a/dev-packages/e2e-tests/test-applications/deno/tests/streamed/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/deno/tests/streamed/transactions.test.ts
@@ -146,6 +146,10 @@ const SEGMENT_SPAN = {
type: 'string',
value: 'http:',
},
+ 'user.ip_address': {
+ type: 'string',
+ value: expect.any(String),
+ },
'user_agent.original': {
type: 'string',
value: 'node',
diff --git a/dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts
index 9de243f3cab3..3d6739e329f1 100644
--- a/dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/src/app.ts
@@ -6,6 +6,7 @@ import * as Cause from 'effect/Cause';
import * as Layer from 'effect/Layer';
import * as Logger from 'effect/Logger';
import * as LogLevel from 'effect/LogLevel';
+import * as Tracer from 'effect/Tracer';
import { createServer } from 'http';
const SentryLive = Layer.mergeAll(
@@ -42,6 +43,31 @@ const router = HttpRouter.empty.pipe(
}),
),
+ HttpRouter.get(
+ '/test-root-span',
+ Effect.gen(function* () {
+ yield* Effect.void.pipe(Effect.withSpan('root-span-request-marker'));
+ yield* Effect.sleep('10 millis').pipe(Effect.withSpan('detached-root-span', { root: true }));
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }),
+ ),
+
+ HttpRouter.get(
+ '/test-external-parent',
+ Effect.gen(function* () {
+ yield* Effect.sleep('10 millis').pipe(
+ Effect.withSpan('continued-span', {
+ parent: Tracer.externalSpan({
+ traceId: 'fedcba0987654321fedcba0987654321',
+ spanId: '0987654321fedcba',
+ sampled: true,
+ }),
+ }),
+ );
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }).pipe(Effect.provide(Sentry.SentryEffectExternalSpanLayer)),
+ ),
+
HttpRouter.get(
'/test-error',
Effect.gen(function* () {
diff --git a/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts
index 9f689d35018b..4025bacdbfba 100644
--- a/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-3-node/tests/spans.test.ts
@@ -65,3 +65,41 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL
expect(child.trace_id).toBe(segment.trace_id);
}
});
+
+test('Sends a root: true span as its own segment in a new trace', async ({ baseURL }) => {
+ const requestSpansPromise = collectStreamedSpans(
+ 'effect-3-node',
+ spans =>
+ spans.some(
+ span =>
+ span.is_segment &&
+ getSpanOp(span) === 'http.server' &&
+ span.attributes['url.path']?.value === '/test-root-span',
+ ) && spans.some(span => span.name === 'root-span-request-marker'),
+ );
+ const detachedSpanPromise = waitForStreamedSpan('effect-3-node', span => span.name === 'detached-root-span');
+
+ await fetch(`${baseURL}/test-root-span`);
+
+ const [requestSpans, detachedSpan] = await Promise.all([requestSpansPromise, detachedSpanPromise]);
+ const segment = requestSpans.find(span => span.is_segment && getSpanOp(span) === 'http.server')!;
+ expect(segment.name).toBe('http.server GET');
+ expect(requestSpans.filter(span => !span.is_segment).map(span => span.name)).toEqual(['root-span-request-marker']);
+
+ expect(detachedSpan.is_segment).toBe(true);
+ expect(detachedSpan.parent_span_id).toBeUndefined();
+ expect(detachedSpan.trace_id).not.toBe(segment.trace_id);
+});
+
+test('Continues the trace of a Tracer.externalSpan parent with the external span layer', async ({ baseURL }) => {
+ const spanPromise = waitForStreamedSpan('effect-3-node', span => span.name === 'continued-span');
+
+ await fetch(`${baseURL}/test-external-parent`);
+
+ const span = await spanPromise;
+ expect(span).toMatchObject({
+ is_segment: true,
+ trace_id: 'fedcba0987654321fedcba0987654321',
+ parent_span_id: '0987654321fedcba',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json b/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json
index f3f429c25b9f..17bd84eba21c 100644
--- a/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json
+++ b/dev-packages/e2e-tests/test-applications/effect-4-browser/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@sentry/effect": "latest || *",
"@types/node": "^18.19.1",
- "effect": "^4.0.0-beta.50",
+ "effect": "^4.0.0",
"typescript": "~5.0.0"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/package.json b/dev-packages/e2e-tests/test-applications/effect-4-node/package.json
index 460bfb57ec45..a639956c51fb 100644
--- a/dev-packages/e2e-tests/test-applications/effect-4-node/package.json
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/package.json
@@ -12,10 +12,10 @@
"test:assert": "pnpm test"
},
"dependencies": {
- "@effect/platform-node": "^4.0.0-rc.118",
+ "@effect/platform-node": "^4.0.0",
"@sentry/effect": "file:../../packed/sentry-effect-packed.tgz",
"@types/node": "^18.19.1",
- "effect": "^4.0.0-rc.118",
+ "effect": "^4.0.0",
"typescript": "~5.0.0"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts
index db5b1fae44eb..0bcc70088359 100644
--- a/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/src/app.ts
@@ -101,6 +101,33 @@ const Routes = Layer.mergeAll(
}),
),
+ HttpRouter.add(
+ 'GET',
+ '/test-root-span',
+ Effect.gen(function* () {
+ yield* Effect.void.pipe(Effect.withSpan('root-span-request-marker'));
+ yield* Effect.sleep('10 millis').pipe(Effect.withSpan('detached-root-span', { root: true }));
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }),
+ ),
+
+ HttpRouter.add(
+ 'GET',
+ '/test-external-parent',
+ Effect.gen(function* () {
+ yield* Effect.sleep('10 millis').pipe(
+ Effect.withSpan('continued-span', {
+ parent: Tracer.externalSpan({
+ traceId: 'fedcba0987654321fedcba0987654321',
+ spanId: '0987654321fedcba',
+ sampled: true,
+ }),
+ }),
+ );
+ return yield* HttpServerResponse.json({ status: 'ok' });
+ }).pipe(Effect.provide(Sentry.SentryEffectExternalSpanLayer)),
+ ),
+
HttpRouter.add(
'GET',
'/test-error',
diff --git a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts
index 2a468928c798..395f5263f286 100644
--- a/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts
+++ b/dev-packages/e2e-tests/test-applications/effect-4-node/tests/spans.test.ts
@@ -65,3 +65,41 @@ test('Sends Effect spans with correct parent-child structure', async ({ baseURL
expect(child.trace_id).toBe(segment.trace_id);
}
});
+
+test('Sends a root: true span as its own segment in a new trace', async ({ baseURL }) => {
+ const requestSpansPromise = collectStreamedSpans(
+ 'effect-4-node',
+ spans =>
+ spans.some(
+ span =>
+ span.is_segment &&
+ getSpanOp(span) === 'http.server' &&
+ span.attributes['url.path']?.value === '/test-root-span',
+ ) && spans.some(span => span.name === 'root-span-request-marker'),
+ );
+ const detachedSpanPromise = waitForStreamedSpan('effect-4-node', span => span.name === 'detached-root-span');
+
+ await fetch(`${baseURL}/test-root-span`);
+
+ const [requestSpans, detachedSpan] = await Promise.all([requestSpansPromise, detachedSpanPromise]);
+ const segment = requestSpans.find(span => span.is_segment && getSpanOp(span) === 'http.server')!;
+ expect(segment.name).toBe('http.server GET');
+ expect(requestSpans.filter(span => !span.is_segment).map(span => span.name)).toEqual(['root-span-request-marker']);
+
+ expect(detachedSpan.is_segment).toBe(true);
+ expect(detachedSpan.parent_span_id).toBeUndefined();
+ expect(detachedSpan.trace_id).not.toBe(segment.trace_id);
+});
+
+test('Continues the trace of a Tracer.externalSpan parent with the external span layer', async ({ baseURL }) => {
+ const spanPromise = waitForStreamedSpan('effect-4-node', span => span.name === 'continued-span');
+
+ await fetch(`${baseURL}/test-external-parent`);
+
+ const span = await spanPromise;
+ expect(span).toMatchObject({
+ is_segment: true,
+ trace_id: 'fedcba0987654321fedcba0987654321',
+ parent_span_id: '0987654321fedcba',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json
index 5f574018e1e0..87d584397cd9 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-basepath/package.json
@@ -15,7 +15,7 @@
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "^15",
+ "next": "^15.5.24",
"react": "latest",
"react-dom": "latest",
"typescript": "~5.0.0"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
index f3bd1c6360f6..32b395b278a5 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-intl/package.json
@@ -15,7 +15,7 @@
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "15.5.21",
+ "next": "15.5.24",
"next-intl": "^4.3.12",
"react": "latest",
"react-dom": "latest",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json
index b03150a518a4..6c8a74f4b432 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15-t3/package.json
@@ -20,7 +20,7 @@
"@trpc/client": "~11.8.0",
"@trpc/react-query": "~11.8.0",
"@trpc/server": "~11.8.0",
- "next": "^15.5.13",
+ "next": "^15.5.24",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"server-only": "^0.0.1",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index 1ca1e3f1e6ed..f6013d99cd95 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -20,7 +20,7 @@
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
"ai": "^3.0.0",
- "next": "15.5.21",
+ "next": "15.5.24",
"react": "latest",
"react-dom": "latest",
"typescript": "~5.0.0",
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 5025d7d2e104..9c6245a06f5c 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
@@ -3,15 +3,12 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
// Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the
// filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`.
-// Not implemented yet — every test is `test.fail()` with the final expected assertions.
// A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id]
-// param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the
-// cache handlers, there is no `cache.get` span at all — then this stays failing until Next
-// exposes RDC reads.
+// param. These runtime-filled entries are read through the cache handlers here; entries served
+// from the prerendered shell (Resume Data Cache) would produce no `cache.get` span at all until
+// Next exposes RDC reads.
test('links a cached layout hit to the trace that filled the layout entry', async ({ request }) => {
- test.fail();
-
const id = crypto.randomUUID();
const missTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
@@ -56,8 +53,6 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn
test('links a cached leaf component under dynamic layouts to the trace that filled the leaf entry', async ({
request,
}) => {
- test.fail();
-
const id = crypto.randomUUID();
const missTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
@@ -101,7 +96,6 @@ test('links a cached leaf component under dynamic layouts to the trace that fill
// spans point at two different origin traces.
test('links two cached levels to different origin traces after the layout expires', async ({ request }) => {
test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production');
- test.fail();
const id = crypto.randomUUID();
@@ -180,8 +174,6 @@ test('links two cached levels to different origin traces after the layout expire
test('links a shared layout hit on a sibling route to the trace of the route that filled the entry', async ({
request,
}) => {
- test.fail();
-
const id = crypto.randomUUID();
const fillTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-page.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-page.spec.ts
index a0acf25df078..6d3f36e297d0 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-page.spec.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-page.spec.ts
@@ -4,15 +4,13 @@ import { waitForTransaction } from '@sentry-internal/test-utils';
// Origin links for `use cache` inside rendered pages (cached components and nested cached
// functions). Target behavior: a cache hit records a `cache.get` span carrying a
// `sentry.link.type: 'cache_origin'` span link to the `cache.put` span of the trace that filled
-// the entry. Not implemented yet — every test is `test.fail()`.
+// the entry.
// Two cached sibling components are two cache entries (props are part of the key), so one request
-// carries one `cache.get` hit span per section, each linking to its own fill. The components sit
-// in a dynamic hole: entries served from the prerendered shell (Resume Data Cache) never reach
-// the cache handlers and produce no spans until Next.js exposes RDC reads.
+// carries one `cache.get` hit span per section, each linking to its own fill. These runtime-filled entries are read
+// through the cache handlers; entries served from the prerendered shell (Resume Data Cache) would
+// produce no spans at all until Next.js exposes RDC reads.
test('links each sibling component hit to the fill of its own entry', async ({ request }) => {
- test.fail();
-
const id = crypto.randomUUID();
const missTxPromise = waitForTransaction('nextjs-16-cacheComponents', transactionEvent => {
@@ -65,7 +63,6 @@ test('links each sibling component hit to the fill of its own entry', async ({ r
test("links a nested cache hit inside another entry's refill to the original fill trace", async ({ request }) => {
test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production');
- test.fail();
const id = crypto.randomUUID();
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts
index 2f604f0402f3..e1e7a468f7cd 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts
@@ -1,14 +1,11 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
-// Origin links for `use cache` in route handlers. Target behavior: a cache hit records a
-// `cache.get` span carrying a `sentry.link.type: 'cache_origin'` span link to the `cache.put`
-// span of the trace that filled the entry; unknown origin means no link. Not implemented yet —
-// every test is `test.fail()`; shipping the feature should only require deleting those lines.
+// Origin links for `use cache` in route handlers: a cache hit records a `cache.get` span
+// carrying a `sentry.link.type: 'cache_origin'` span link to the `cache.put` span of the trace
+// that filled the entry; unknown origin means no link.
test('links a route handler cache hit to the trace that filled the entry', async ({ request }) => {
- test.fail();
-
// 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.
const id = crypto.randomUUID();
@@ -55,7 +52,6 @@ test('links a route handler cache hit to the trace that filled the entry', async
test('moves the origin link to the refill trace after the entry expires', async ({ request }) => {
test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production');
- test.fail();
const id = crypto.randomUUID();
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 d456686f24ec..abd984c429dc 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
@@ -4,15 +4,12 @@ import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks-
// Origin links (`sentry.link.type: 'cache_origin'` on `cache.get` hit spans, pointing at the
// filling `cache.put`) for `use cache` in nested layout trees under `app/(cached-nesting)/`.
-// Not implemented yet — every test is `test.fail()` with the final expected assertions.
// A `use cache` layout between dynamic segments. The layout entry is keyed by the awaited [id]
-// param. If Next serves the entry from the prerendered shell (Resume Data Cache) instead of the
-// cache handlers, there is no `cache.get` span at all — then this stays failing until Next
-// exposes RDC reads.
+// param. These runtime-filled entries are read through the cache handlers here; entries served
+// from the prerendered shell (Resume Data Cache) would produce no `cache.get` span at all until
+// Next exposes RDC reads.
test('links a cached layout hit to the trace that filled the layout entry', async ({ request }) => {
- test.fail();
-
const id = crypto.randomUUID();
const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
@@ -57,8 +54,6 @@ test('links a cached layout hit to the trace that filled the layout entry', asyn
test('links a cached leaf component under dynamic layouts to the trace that filled the leaf entry', async ({
request,
}) => {
- test.fail();
-
const id = crypto.randomUUID();
const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
@@ -102,7 +97,6 @@ test('links a cached leaf component under dynamic layouts to the trace that fill
// spans point at two different origin traces.
test('links two cached levels to different origin traces after the layout expires', async ({ request }) => {
test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production');
- test.fail();
const id = crypto.randomUUID();
@@ -185,8 +179,6 @@ test('links two cached levels to different origin traces after the layout expire
test('links a shared layout hit on a sibling route to the trace of the route that filled the entry', async ({
request,
}) => {
- test.fail();
-
const id = crypto.randomUUID();
const fillSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-page.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-page.spec.ts
index fd8a86a5c848..0bac1aeec817 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-page.spec.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-page.spec.ts
@@ -5,15 +5,13 @@ import { CACHE_ORIGIN_LINK_ATTRIBUTES } from './cacheOriginLinks-utils';
// Origin links for `use cache` inside rendered pages (cached components and nested cached
// functions). Target behavior: a cache hit records a `cache.get` span carrying a
// `sentry.link.type: 'cache_origin'` span link to the `cache.put` span of the trace that filled
-// the entry. Not implemented yet — every test is `test.fail()`.
+// the entry.
// Two cached sibling components are two cache entries (props are part of the key), so one request
-// carries one `cache.get` hit span per section, each linking to its own fill. The components sit
-// in a dynamic hole: entries served from the prerendered shell (Resume Data Cache) never reach
-// the cache handlers and produce no spans until Next.js exposes RDC reads.
+// carries one `cache.get` hit span per section, each linking to its own fill. These runtime-filled entries are read
+// through the cache handlers; entries served from the prerendered shell (Resume Data Cache) would
+// produce no spans at all until Next.js exposes RDC reads.
test('links each sibling component hit to the fill of its own entry', async ({ request }) => {
- test.fail();
-
const id = crypto.randomUUID();
const missSpansPromise = collectStreamedSpans('nextjs-16-streaming-cacheComponents', spansOfTrace => {
@@ -70,7 +68,6 @@ test('links each sibling component hit to the fill of its own entry', async ({ r
test("links a nested cache hit inside another entry's refill to the original fill trace", async ({ request }) => {
test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production');
- test.fail();
const id = crypto.randomUUID();
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts
index 09148406915d..cf1d5351d49b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOriginLinks-routeHandler.spec.ts
@@ -2,14 +2,11 @@ import { expect, test } from '@playwright/test';
import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
import { CACHE_ORIGIN_LINK_ATTRIBUTES, findCacheSpan } from './cacheOriginLinks-utils';
-// Origin links for `use cache` in route handlers. Target behavior: a cache hit records a
-// `cache.get` span carrying a `sentry.link.type: 'cache_origin'` span link to the `cache.put`
-// span of the trace that filled the entry; unknown origin means no link. Not implemented yet —
-// every test is `test.fail()`; shipping the feature should only require deleting those lines.
+// Origin links for `use cache` in route handlers: a cache hit records a `cache.get` span
+// carrying a `sentry.link.type: 'cache_origin'` span link to the `cache.put` span of the trace
+// that filled the entry; unknown origin means no link.
test('links a route handler cache hit to the trace that filled the entry', async ({ request }) => {
- test.fail();
-
// 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.
const id = crypto.randomUUID();
@@ -56,7 +53,6 @@ test('links a route handler cache hit to the trace that filled the entry', async
test('moves the origin link to the refill trace after the entry expires', async ({ request }) => {
test.skip(process.env.TEST_ENV !== 'production', 'Entries are only discarded at `expire` in production');
- test.fail();
const id = crypto.randomUUID();
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts
new file mode 100644
index 000000000000..1bb06c6e1a6d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/flush.ts
@@ -0,0 +1,7 @@
+import { flush } from '@sentry/nitro';
+import { defineHandler } from 'nitro/h3';
+
+export default defineHandler(async () => {
+ await flush();
+ return { status: 'ok' };
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts
new file mode 100644
index 000000000000..760a6ad17852
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/server/api/test-error-400.ts
@@ -0,0 +1,5 @@
+import { defineHandler, HTTPError } from 'nitro/h3';
+
+export default defineHandler(() => {
+ throw new HTTPError({ status: 400, message: 'Explicit 400 test error' });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts
index 1441ecd5ecab..18cbd7ca6709 100644
--- a/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts
@@ -1,16 +1,10 @@
import { expect, test } from '@playwright/test';
-import { waitForError } from '@sentry-internal/test-utils';
+import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
test('Sends an error event to Sentry', async ({ request }) => {
- // The thrown error is reported twice: once via the h3 tracing channel and once via Nitro's `error`
- // hook (which sees it wrapped in an `HTTPError`). Match on the mechanism so we deterministically
- // await the event under test instead of whichever arrives first.
const errorEventPromise = waitForError('nitro-3', event => {
- return (
- !event.type &&
- !!event.exception?.values?.some(
- v => v.value === 'This is a test error' && v.mechanism?.type === 'auto.http.nitro.onTraceError',
- )
+ return !!event.exception?.values?.some(
+ v => v.value === 'This is a test error' && v.mechanism?.type === 'auto.function.nitro.captureErrorHook',
);
});
@@ -20,16 +14,45 @@ test('Sends an error event to Sentry', async ({ request }) => {
const errorEvent = await errorEventPromise;
- expect(errorEvent.exception?.values).toHaveLength(1);
-
- expect(errorEvent.exception?.values?.[0]?.type).toBe('Error');
- expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error');
- expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
+ expect(errorEvent.exception?.values).toEqual([
+ expect.objectContaining({
+ type: 'Error',
+ value: 'This is a test error',
+ }),
expect.objectContaining({
- handled: false,
- type: 'auto.http.nitro.onTraceError',
+ type: 'HTTPError',
+ value: 'This is a test error',
+ mechanism: expect.objectContaining({
+ handled: false,
+ type: 'auto.function.nitro.captureErrorHook',
+ }),
}),
- );
+ ]);
+});
+
+test('Does not send an explicitly thrown 400 error to Sentry', async ({ request }) => {
+ let errorReceived = false;
+
+ void waitForError('nitro-3', event => {
+ if (event.exception?.values?.some(v => v.value === 'Explicit 400 test error')) {
+ errorReceived = true;
+ return true;
+ }
+ return false;
+ });
+
+ const flushSpanPromise = waitForStreamedSpan('nitro-3', span => {
+ return span.is_segment && span.name === 'GET /api/flush';
+ });
+
+ const response = await request.get('/api/test-error-400');
+ expect(response.status()).toBe(400);
+
+ const flushResponse = await request.get('/api/flush');
+ expect(flushResponse.status()).toBe(200);
+ await flushSpanPromise;
+
+ expect(errorReceived).toBe(false);
});
test('Does not send 404 errors to Sentry', async ({ request }) => {
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/.gitignore b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/.gitignore
new file mode 100644
index 000000000000..795f6b6d40d1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+pnpm-lock.yaml
+dist
+test-results
+playwright-report
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/package.json b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/package.json
new file mode 100644
index 000000000000..ef1b771dd545
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "node-anthropic-send-to-sentry",
+ "description": "The Anthropic integration as a user runs it: a plain Sentry.init on an express app, real chat, streaming, stream-helper and tool-call requests through OpenRouter, and the gen_ai spans read back from a real Sentry project",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc",
+ "start": "node --import ./dist/instrument.js dist/app.js",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules dist pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-latest": "pnpm install && pnpm add @anthropic-ai/sdk@latest && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@anthropic-ai/sdk": "0.63.0",
+ "@sentry/node": "file:../../packed/sentry-node-packed.tgz",
+ "express": "^4.21.2"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.63.0",
+ "@types/express": "^4.17.21",
+ "@types/node": "^24.0.0",
+ "typescript": "~5.9.0"
+ },
+ "sentryTest": {
+ "optional": true,
+ "optionalVariants": [
+ {
+ "build-command": "pnpm test:build-latest",
+ "label": "node-anthropic-send-to-sentry (latest)"
+ }
+ ]
+ },
+ "volta": {
+ "node": "24.15.0",
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/playwright.config.ts b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/playwright.config.ts
new file mode 100644
index 000000000000..ef4764ce8d73
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/playwright.config.ts
@@ -0,0 +1,28 @@
+import { defineConfig } from '@playwright/test';
+
+const port = 3030;
+
+export default defineConfig({
+ testDir: './tests',
+ /*
+ * Spans take ~2min to become queryable via the trace endpoint, and each poll has its own 180s
+ * budget. The first test polls twice in a row (the model span, then its parent), so the ceiling
+ * has to hold two polls back to back.
+ */
+ timeout: 400_000,
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: 0,
+ // Every test spends most of its time polling Sentry, so run them all at once.
+ workers: '100%',
+ reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list',
+ use: {
+ baseURL: `http://localhost:${port}`,
+ },
+ webServer: {
+ command: 'pnpm start',
+ port,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts
new file mode 100644
index 000000000000..7ad878cdff26
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/app.ts
@@ -0,0 +1,132 @@
+// `instrument.ts` is preloaded via `node --import`, so Sentry is already initialised here.
+import * as Sentry from '@sentry/node';
+import Anthropic from '@anthropic-ai/sdk';
+import express from 'express';
+
+const apiKey = process.env.E2E_OPENROUTER_API_KEY;
+if (!apiKey) {
+ throw new Error('E2E_OPENROUTER_API_KEY is not set');
+}
+
+// OpenRouter serves an Anthropic-compatible `/api/v1/messages`, so the stock client only needs a
+// different base URL. It authenticates with a bearer token, so the key goes in `authToken`
+// (Authorization: Bearer) rather than `apiKey` (x-api-key). The model is incidental: what is under
+// test is the SDK's own request/response code path, which is what Sentry instruments.
+const client = new Anthropic({ authToken: apiKey, baseURL: 'https://openrouter.ai/api' });
+
+const MODEL = 'openai/gpt-4o-mini';
+
+const SHORT_ANSWER = 'Answer in at most five words.';
+const CHAT_PROMPT = `What is the capital of France? ${SHORT_ANSWER}`;
+// Deliberately does not name the tool: `tool_choice` forces the call.
+const WEATHER_PROMPT = `What is the weather in Paris? ${SHORT_ANSWER}`;
+const SYSTEM = 'You are a helpful assistant used by an automated test.';
+
+const WEATHER_TOOL = {
+ name: 'get_weather',
+ description: 'Get the current weather for a city.',
+ input_schema: {
+ type: 'object' as const,
+ properties: { city: { type: 'string', description: 'The city name' } },
+ required: ['city'],
+ },
+};
+
+const app = express();
+
+/** The trace this request is recorded under, so the test can look its spans up in Sentry. */
+function currentTraceId(): string | undefined {
+ return Sentry.getActiveSpan()?.spanContext().traceId;
+}
+
+/** The text of a message's first text block. */
+function textOf(message: Anthropic.Message): string {
+ const first = message.content[0];
+ return first?.type === 'text' ? first.text : '';
+}
+
+// Each route makes one call through the `@anthropic-ai/sdk` client and answers with the trace id, so
+// the test can find the request's spans in Sentry.
+
+app.get('/chat', async (_req, res, next) => {
+ try {
+ const message = await client.messages.create({
+ model: MODEL,
+ max_tokens: 32,
+ temperature: 0,
+ system: SYSTEM,
+ messages: [{ role: 'user', content: CHAT_PROMPT }],
+ });
+ res.send({ traceId: currentTraceId(), answer: textOf(message) });
+ } catch (error) {
+ next(error);
+ }
+});
+
+// `messages.create({ stream: true })` returns an async iterable of events the caller drains.
+app.get('/chat-stream', async (_req, res, next) => {
+ try {
+ const stream = await client.messages.create({
+ model: MODEL,
+ max_tokens: 32,
+ temperature: 0,
+ system: SYSTEM,
+ messages: [{ role: 'user', content: CHAT_PROMPT }],
+ stream: true,
+ });
+
+ let answer = '';
+ for await (const event of stream) {
+ if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
+ answer += event.delta.text;
+ }
+ }
+ res.send({ traceId: currentTraceId(), answer });
+ } catch (error) {
+ next(error);
+ }
+});
+
+// `messages.stream()` is the SDK's streaming helper: it accumulates the events into the final
+// message, and the integration instruments it separately from `create`.
+app.get('/stream-helper', async (_req, res, next) => {
+ try {
+ const message = await client.messages
+ .stream({
+ model: MODEL,
+ max_tokens: 32,
+ temperature: 0,
+ system: SYSTEM,
+ messages: [{ role: 'user', content: CHAT_PROMPT }],
+ })
+ .finalMessage();
+ res.send({ traceId: currentTraceId(), answer: textOf(message) });
+ } catch (error) {
+ next(error);
+ }
+});
+
+app.get('/tools', async (_req, res, next) => {
+ try {
+ const message = await client.messages.create({
+ model: MODEL,
+ max_tokens: 64,
+ messages: [{ role: 'user', content: WEATHER_PROMPT }],
+ tools: [WEATHER_TOOL],
+ tool_choice: { type: 'tool', name: 'get_weather' },
+ });
+ res.send({ traceId: currentTraceId(), toolUse: message.content.filter(block => block.type === 'tool_use') });
+ } catch (error) {
+ next(error);
+ }
+});
+
+app.use((error: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
+ res.status(500).send({ message: error.message });
+});
+
+const port = Number(process.env.PORT ?? 3030);
+app.listen(port, () => {
+ // eslint-disable-next-line no-console
+ console.log(`node-anthropic-send-to-sentry listening on port ${port}`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/instrument.ts
new file mode 100644
index 000000000000..7d031b0e6d8e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/src/instrument.ts
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/node';
+
+// The setup from the docs, nothing more: preloaded with `node --import`, no tunnel, so the spans go to
+// the real Sentry project behind `E2E_TEST_DSN`, where the tests read them back. The Anthropic integration
+// is on by default; its runtime channel injection is what picks up the `@anthropic-ai/sdk` client in app.ts.
+Sentry.init({
+ dsn: process.env.E2E_TEST_DSN,
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ tracesSampleRate: 1,
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tests/send-to-sentry.test.ts
new file mode 100644
index 000000000000..44831a6d1a59
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tests/send-to-sentry.test.ts
@@ -0,0 +1,152 @@
+import { expect, test } from '@playwright/test';
+import type { TraceItem } from './utils/sentry-api';
+import {
+ EVENT_POLLING_OPTIONS,
+ fetchSpanAttributes,
+ fetchTrace,
+ findSpanInTrace,
+ flattenTrace,
+} from './utils/sentry-api';
+
+// Mirrors src/app.ts.
+const MODEL = 'openai/gpt-4o-mini';
+
+type Attributes = Record;
+
+/** Sends one request to the app and returns the trace its request span was recorded under. */
+async function requestTrace(baseURL: string | undefined, path: string): Promise {
+ const response = await fetch(`${baseURL}${path}`);
+ const body = await response.text();
+ expect(response.status, `${path} answered ${response.status}: ${body}`).toBe(200);
+
+ const { traceId } = JSON.parse(body) as { traceId?: string };
+ expect(traceId, `${path} did not report a trace id: ${body}`).toMatch(/^[0-9a-f]{32}$/);
+
+ console.log(`${path}: https://${process.env.E2E_TEST_SENTRY_ORG_SLUG}.sentry.io/explore/traces/trace/${traceId}/`);
+ return traceId!;
+}
+
+/**
+ * Polls Sentry until the trace holds a span with `op`, then returns that span and its attributes. The
+ * trace endpoint only carries the span's op, name and place in the tree; the attributes need a second
+ * lookup, which is polled as well since the two can land apart.
+ */
+async function waitForSpan(traceId: string, op: string): Promise<{ span: TraceItem; attributes: Attributes }> {
+ let found: { span: TraceItem; attributes: Attributes } | undefined;
+
+ await expect
+ .poll(async () => {
+ const span = await findSpanInTrace(traceId, op);
+ const attributes = span?.event_id ? await fetchSpanAttributes(traceId, span.event_id) : undefined;
+ found = span && attributes ? { span, attributes } : undefined;
+ return found;
+ }, EVENT_POLLING_OPTIONS)
+ .toBeDefined();
+
+ return found!;
+}
+
+/** Whether the model-call span sits somewhere below the request span (`http.server`) of the trace. */
+async function isModelSpanUnderRequestSpan(traceId: string, modelSpanId: string): Promise {
+ const requestSpan = flattenTrace(await fetchTrace(traceId)).find(item => item.op === 'http.server');
+ return flattenTrace(requestSpan?.children ?? []).some(item => item.event_id === modelSpanId);
+}
+
+/**
+ * The attributes every successful model-call span carries once Sentry has stored it, whatever the
+ * model answers. Model-dependent values (token counts, response id and model) are checked for shape,
+ * not content. Note the names are the stored ones: `origin` and `span.status` rather than the
+ * `sentry.*` attributes the SDK sends.
+ */
+function expectModelCallAttributes(span: TraceItem, attributes: Attributes): void {
+ expect(span.description).toBe(`chat ${MODEL}`);
+ expect(attributes).toMatchObject({
+ 'span.op': 'gen_ai.chat',
+ 'span.status': 'ok',
+ origin: 'auto.ai.anthropic',
+ 'gen_ai.operation.name': 'chat',
+ 'gen_ai.provider.name': 'anthropic',
+ 'gen_ai.request.model': MODEL,
+ });
+ expect(typeof attributes['gen_ai.response.id']).toBe('string');
+ expect(typeof attributes['gen_ai.response.model']).toBe('string');
+ expect(attributes['gen_ai.usage.output_tokens']).toBeGreaterThan(0);
+ expect(attributes['gen_ai.usage.total_tokens']).toBeGreaterThan(0);
+}
+
+/** The prompt and the answer, which the SDK records by default and which have to survive the trip. */
+function expectRecordedConversation(attributes: Attributes): void {
+ // The answer is sent as `gen_ai.response.text` and stored as `gen_ai.output.messages`.
+ expect(attributes['gen_ai.system_instructions']).toContain('automated test');
+ expect(attributes['gen_ai.input.messages']).toContain('capital of France');
+ expect(typeof attributes['gen_ai.output.messages']).toBe('string');
+}
+
+/** `gen_ai.response.finish_reasons` is a JSON array, only recorded for streamed calls. */
+function finishReasons(attributes: Attributes): string[] {
+ const raw = attributes['gen_ai.response.finish_reasons'];
+ expect(typeof raw, 'gen_ai.response.finish_reasons').toBe('string');
+ return JSON.parse(raw as string);
+}
+
+test('Sends a message to Sentry as a gen_ai.chat span under the request span', async ({ baseURL }) => {
+ const traceId = await requestTrace(baseURL, '/chat');
+
+ const { span, attributes } = await waitForSpan(traceId, 'gen_ai.chat');
+
+ expectModelCallAttributes(span, attributes);
+ expect(attributes['gen_ai.usage.input_tokens']).toBeGreaterThan(0);
+ expect(attributes['gen_ai.request.temperature']).toBe(0);
+ expect(attributes['gen_ai.response.streaming']).toBeUndefined();
+ expectRecordedConversation(attributes);
+
+ // The request span is the segment; it ends last, so it can land after its children.
+ await expect.poll(() => isModelSpanUnderRequestSpan(traceId, span.event_id!), EVENT_POLLING_OPTIONS).toBe(true);
+});
+
+test('Sends a streamed message to Sentry with its token usage', async ({ baseURL }) => {
+ const traceId = await requestTrace(baseURL, '/chat-stream');
+
+ const { span, attributes } = await waitForSpan(traceId, 'gen_ai.chat');
+
+ expectModelCallAttributes(span, attributes);
+ expect(attributes['gen_ai.response.streaming']).toBe(true);
+ expect(finishReasons(attributes)).toContain('end_turn');
+ expectRecordedConversation(attributes);
+ // The input usage is not asserted: the instrumentation takes it from `message_start`, where
+ // OpenRouter reports 0, and ignores the real count OpenRouter (and the Anthropic API) put in
+ // `message_delta`. Output usage comes from `message_delta` and is covered by the shared checks.
+});
+
+test('Sends a message made with the stream helper to Sentry', async ({ baseURL }) => {
+ const traceId = await requestTrace(baseURL, '/stream-helper');
+
+ const { span, attributes } = await waitForSpan(traceId, 'gen_ai.chat');
+
+ // `messages.stream()` calls `create` underneath, which must not produce a second span. The parent
+ // lands last, so count after it; the short poll only rides out a dropped connection.
+ await expect.poll(() => isModelSpanUnderRequestSpan(traceId, span.event_id!), EVENT_POLLING_OPTIONS).toBe(true);
+ await expect
+ .poll(async () => flattenTrace(await fetchTrace(traceId)).filter(item => item.op === 'gen_ai.chat').length, {
+ timeout: 30_000,
+ intervals: [5_000],
+ })
+ .toBe(1);
+ expectModelCallAttributes(span, attributes);
+ expect(attributes['gen_ai.response.streaming']).toBe(true);
+ expect(finishReasons(attributes)).toContain('end_turn');
+ expectRecordedConversation(attributes);
+});
+
+test('Sends a message that returned a tool use to Sentry', async ({ baseURL }) => {
+ const traceId = await requestTrace(baseURL, '/tools');
+
+ const { span, attributes } = await waitForSpan(traceId, 'gen_ai.chat');
+
+ expectModelCallAttributes(span, attributes);
+ expect(attributes['gen_ai.usage.input_tokens']).toBeGreaterThan(0);
+ expect(attributes['gen_ai.tool.definitions']).toContain('get_weather');
+ expect(attributes['gen_ai.input.messages']).toContain('weather in Paris');
+ // The SDK also sends the returned tool use as `gen_ai.response.tool_calls`, but that attribute is
+ // not stored under that name, and stop reasons are only recorded for streamed calls.
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tests/utils/sentry-api.ts b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tests/utils/sentry-api.ts
new file mode 100644
index 000000000000..521b830eefcb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tests/utils/sentry-api.ts
@@ -0,0 +1,131 @@
+const authToken = process.env.E2E_TEST_AUTH_TOKEN;
+const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
+const sentryTestProject = process.env.E2E_TEST_SENTRY_PROJECT;
+
+/**
+ * Spans only become queryable once they have made it through to EAP, which takes
+ * noticeably longer than the error pipeline (~2min vs ~20s when this was measured).
+ */
+export const EVENT_POLLING_OPTIONS = { timeout: 180_000, intervals: [5_000] };
+
+/**
+ * A node of the span tree returned by the organization trace endpoint. Spans, errors and
+ * occurrences all share this shape and are discriminated by `event_type`.
+ */
+export interface TraceItem {
+ /** On a span this is the span id. */
+ event_id?: string;
+ event_type?: 'span' | 'error' | 'occurrence' | 'uptime_check';
+ op?: string;
+ /** On a span this is the span name. */
+ description?: string;
+ children?: TraceItem[];
+ errors?: TraceItem[];
+ occurrences?: TraceItem[];
+}
+
+/**
+ * One request to the Sentry API with the E2E token. Returns `undefined` when the connection
+ * fails or drops mid-response: over the minutes a test polls, the API closes the odd
+ * connection ("TypeError: terminated"), and an error thrown inside `expect.poll` fails the
+ * test instead of letting it retry, so a network error is treated like a "not there yet".
+ */
+async function fetchSentryApi(what: string, path: string): Promise<{ status: number; body: string } | undefined> {
+ try {
+ const response = await fetch(`https://sentry.io/api/0/${path}`, {
+ headers: { Authorization: `Bearer ${authToken}` },
+ });
+ return { status: response.status, body: await response.text() };
+ } catch (error) {
+ console.log(`${what} failed: ${error}`);
+ return undefined;
+ }
+}
+
+export async function fetchTrace(traceId: string): Promise {
+ const what = `Trace lookup for ${traceId}`;
+ const response = await fetchSentryApi(what, `organizations/${sentryTestOrgSlug}/trace/${traceId}/?statsPeriod=1h`);
+ if (!response) {
+ return [];
+ }
+
+ // The trace endpoint is org scoped, so the auth token needs `org:read` on top of the
+ // project scopes the other assertions rely on. That never resolves by waiting, so fail
+ // loudly instead of polling until the timeout and reporting it as a missing event.
+ if (response.status === 401 || response.status === 403) {
+ throw new Error(
+ `${what} was rejected with ${response.status}: ${response.body}. ` +
+ 'E2E_TEST_AUTH_TOKEN needs the `org:read` scope.',
+ );
+ }
+
+ // Empty traces and the occasional rate limit are expected while polling, so treat anything
+ // else that is not a success as "not there yet" -- but log it, since a rejected request and
+ // a trace that has not landed are otherwise indistinguishable.
+ if (response.status !== 200) {
+ console.log(`${what} returned ${response.status}: ${response.body}`);
+ return [];
+ }
+
+ return JSON.parse(response.body);
+}
+
+/**
+ * Errors attach to whichever span was active when they were captured, and relocate from the
+ * top level into that span once it lands, so a given event can surface at any depth.
+ */
+export function flattenTrace(items: TraceItem[]): TraceItem[] {
+ return items.flatMap(item => [
+ item,
+ ...flattenTrace(item.children ?? []),
+ ...flattenTrace(item.errors ?? []),
+ ...flattenTrace(item.occurrences ?? []),
+ ]);
+}
+
+export async function findSpanInTrace(traceId: string, op: string): Promise {
+ return flattenTrace(await fetchTrace(traceId)).find(item => item.op === op);
+}
+
+/**
+ * The attributes of one span, keyed by name, as Sentry stores them. The trace endpoint only carries
+ * a span's op, name and place in the tree; the attributes come from the project trace-items
+ * endpoint. Returns `undefined` while the span has not landed.
+ *
+ * Ingest normalises what the SDK sent: `sentry.origin` comes back as `origin`, `sentry.op` as
+ * `span.op`, `gen_ai.response.text` as `gen_ai.output.messages`, and some numbers are indexed as
+ * tags (`tags[,number]`). Integers are returned as strings and are converted here; everything
+ * else is passed through as is.
+ */
+export async function fetchSpanAttributes(
+ traceId: string,
+ spanId: string,
+): Promise | undefined> {
+ const what = `Span lookup for ${spanId}`;
+ const response = await fetchSentryApi(
+ what,
+ `projects/${sentryTestOrgSlug}/${sentryTestProject}/trace-items/${spanId}/?trace_id=${traceId}&item_type=spans`,
+ );
+ if (!response) {
+ return undefined;
+ }
+
+ if (response.status === 401 || response.status === 403) {
+ throw new Error(
+ `${what} was rejected with ${response.status}: ${response.body}. ` +
+ 'E2E_TEST_AUTH_TOKEN needs the `org:read` scope.',
+ );
+ }
+
+ if (response.status !== 200) {
+ if (response.status !== 404) {
+ console.log(`${what} returned ${response.status}: ${response.body}`);
+ }
+ return undefined;
+ }
+
+ const { attributes } = JSON.parse(response.body) as { attributes?: { name: string; type: string; value: unknown }[] };
+ return Object.fromEntries(
+ (attributes ?? []).map(({ name, type, value }) => [name, type === 'int' ? Number(value) : value]),
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tsconfig.json
new file mode 100644
index 000000000000..795574fd9df2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/node-anthropic-send-to-sentry/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "module": "nodenext",
+ "moduleResolution": "nodenext",
+ "outDir": "dist",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "types": ["node"]
+ },
+ "include": ["src/**/*.ts"]
+}
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 bd74c74d5997..14d64149cefc 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
@@ -20,6 +20,8 @@ const keepClientSourceMaps = process.env.E2E_KEEP_CLIENT_SOURCEMAPS === 'true';
const isStaticBuild = process.env.NUXT_COMMAND === 'generate';
const CLIENT_OUTPUT = path.join('.output', 'public');
+/** Vite's own output directory. Nitro copies it to `CLIENT_OUTPUT`, and Sentry deletes no maps here. */
+const CLIENT_BUILD_OUTPUT = path.join('.nuxt', 'dist', 'client');
const SERVER_OUTPUT = path.join('.output', 'server');
/** Both markers sit in comments, so bundlers strip them from the code but keep them in `sourcesContent`. */
@@ -36,6 +38,26 @@ function filesContaining(dir: string, needle: string): string[] {
.filter(file => fs.readFileSync(file, 'utf8').includes(needle));
}
+/**
+ * Rolldown emits no source map for a chunk without mappable source, such as Vue's export helper.
+ * Such a chunk still gets a debug ID, but there is no map to upload for it.
+ */
+function debugIdsOfChunksWithoutSourceMap(dir: string): Set {
+ const debugIds = new Set();
+
+ for (const file of filesContaining(dir, 'sentry-dbid-')) {
+ if (file.endsWith('.map') || fs.existsSync(`${file}.map`)) {
+ continue;
+ }
+
+ for (const match of fs.readFileSync(file, 'utf8').matchAll(/sentry-dbid-([\da-f-]{36})/gi)) {
+ debugIds.add((match[1] as string).toLowerCase());
+ }
+ }
+
+ return debugIds;
+}
+
console.log(
`Variant: ${isStaticBuild ? 'nuxt generate' : 'nuxt build'}, ` +
`client source maps ${keepClientSourceMaps ? 'kept' : 'deleted'}\n`,
@@ -139,11 +161,15 @@ assert.deepEqual(malformedDebugIds, [], 'Expected every uploaded debug ID to be
// An uploaded map is only reachable at runtime if the shipped bundle claims the same ID. Inspecting
// the upload alone cannot show this.
+const debugIdsWithoutSourceMap = debugIdsOfChunksWithoutSourceMap(CLIENT_BUILD_OUTPUT);
+
for (const outputDir of isStaticBuild ? [CLIENT_OUTPUT] : [CLIENT_OUTPUT, SERVER_OUTPUT]) {
const injectedDebugIds = findInjectedDebugIds({ outputDir });
assert.ok(injectedDebugIds.length > 0, `Expected debug IDs to be injected into ${outputDir}`);
- const unuploaded = injectedDebugIds.filter(debugId => !uploadedDebugIds.has(debugId));
+ const unuploaded = injectedDebugIds.filter(
+ debugId => !uploadedDebugIds.has(debugId) && !debugIdsWithoutSourceMap.has(debugId),
+ );
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`);
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/route-provider/[id].vue b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/route-provider/[id].vue
new file mode 100644
index 000000000000..15ecf3348e94
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/app/pages/route-provider/[id].vue
@@ -0,0 +1,14 @@
+
+
+
+
{{ route }}
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-5/tests/route-provider.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/route-provider.test.ts
new file mode 100644
index 000000000000..4796384fbb47
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-5/tests/route-provider.test.ts
@@ -0,0 +1,8 @@
+import { expect, test } from '@playwright/test';
+
+// The route provider reads the router off the Nuxt app, so this fails if the plugin never registers it.
+test('resolves the parameterized route through the route provider', async ({ page }) => {
+ await page.goto('/route-provider/123');
+
+ await expect(page.locator('#resolved-route')).toHaveText('/route-provider/:id()');
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/tests/performance/trace-propagation.test.ts
index b62e784ff3e3..bcdb5e2f34fa 100644
--- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/tests/performance/trace-propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-static/tests/performance/trace-propagation.test.ts
@@ -19,7 +19,7 @@ test.describe('Trace propagation', () => {
test('should have trace connection', async ({ page }) => {
const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
- return transactionEvent.transaction === 'GET *';
+ return transactionEvent.transaction === 'GET /';
});
const clientTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
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 756115577e68..9d52a907bcbe 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
@@ -17,11 +17,37 @@ function HomePage(handle: Handle>) {
Sentry Remix 3
+ {/* No `data-rmx-document`, so the runtime intercepts this through the Navigation API. */}
+
+ User
+
+
+