From 4169bc6144ed75885314b06b32f2470c267ae6c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 15:43:55 +0200 Subject: [PATCH 01/32] test(deps): Bump hono from 4.12.14 to 4.12.16 (#20712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.14 to 4.12.16.
Release notes

Sourced from hono's releases.

v4.12.16

Security fixes

This release includes fixes for the following security issues:

Unvalidated JSX Tag Names in hono/jsx May Allow HTML Injection

Affects: hono/jsx. Fixes missing validation of JSX tag names when using jsx() or createElement(), which could allow HTML injection if untrusted input is used as the tag name. GHSA-69xw-7hcm-h432

bodyLimit() can be bypassed for chunked / unknown-length requests

Affects: Body Limit Middleware. Fixes late enforcement for request bodies without a reliable Content-Length (e.g. chunked requests), where oversized requests could reach handlers and return successful responses before being rejected. GHSA-9vqf-7f2p-gf9v

v4.12.15

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.12.14...v4.12.15

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.12.14&new-version=4.12.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../bun-integration-tests/package.json | 2 +- .../cloudflare-integration-tests/package.json | 2 +- .../node-integration-tests/package.json | 2 +- yarn.lock | 46 +++++++++++++++++-- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/dev-packages/bun-integration-tests/package.json b/dev-packages/bun-integration-tests/package.json index 114c8f7f7d47..9e44c9c60394 100644 --- a/dev-packages/bun-integration-tests/package.json +++ b/dev-packages/bun-integration-tests/package.json @@ -15,7 +15,7 @@ "dependencies": { "@sentry/bun": "10.52.0", "@sentry/hono": "10.52.0", - "hono": "^4.12.14" + "hono": "^4.12.16" }, "devDependencies": { "@sentry-internal/test-utils": "10.52.0", diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 43b2327a3789..9aaf44103ed9 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -16,7 +16,7 @@ "@langchain/langgraph": "^1.0.1", "@sentry/cloudflare": "10.52.0", "@sentry/hono": "10.52.0", - "hono": "^4.12.14" + "hono": "^4.12.16" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250922.0", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 0e3936dd9fb5..bfa5e3ea85bd 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -58,7 +58,7 @@ "generic-pool": "^3.9.0", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", - "hono": "^4.12.14", + "hono": "^4.12.16", "http-terminator": "^3.2.0", "ioredis": "^5.4.1", "kafkajs": "2.2.4", diff --git a/yarn.lock b/yarn.lock index 5876796341f3..c920dff0d60d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7135,6 +7135,11 @@ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== +"@redis/bloom@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-5.12.1.tgz#047dbfce93cfa7e5879fb58ad1b2afe87ebb2dc4" + integrity sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA== + "@redis/client@1.5.16": version "1.5.16" resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.16.tgz#1d5919077a06a4b935b0e4bef9e036eef1a10371" @@ -7144,6 +7149,13 @@ generic-pool "3.9.0" yallist "4.0.0" +"@redis/client@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-5.12.1.tgz#a35a2bac546c727d7915d2d91b63a77111e51ebd" + integrity sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA== + dependencies: + cluster-key-slot "1.1.2" + "@redis/graph@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea" @@ -7154,16 +7166,31 @@ resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.6.tgz#b7a7725bbb907765d84c99d55eac3fcf772e180e" integrity sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw== +"@redis/json@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-5.12.1.tgz#52aff987abe4d41ec9644857fb00f16c7a97fdb7" + integrity sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg== + "@redis/search@1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.6.tgz#33bcdd791d9ed88ab6910243a355d85a7fedf756" integrity sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw== +"@redis/search@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-5.12.1.tgz#b7a738be918c8a7af91e39c5bd2023f30f392981" + integrity sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w== + "@redis/time-series@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad" integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg== +"@redis/time-series@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-5.12.1.tgz#15b6deaaf3716bc2633311c0ed18201c9299392d" + integrity sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ== + "@remix-run/node-fetch-server@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@remix-run/node-fetch-server/-/node-fetch-server-0.13.0.tgz#93be9c2e0e6f12512be471501e3a86dda295b178" @@ -18871,10 +18898,10 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hono@^4.12.14: - version "4.12.14" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.14.tgz#4777c9512b7c84138e4f09e61e3d2fa305eb1414" - integrity sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w== +hono@^4.12.16: + version "4.12.16" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.16.tgz#90fcc63caa713199703bda08d8518f654e98b516" + integrity sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg== hookable@^5.5.3: version "5.5.3" @@ -26085,6 +26112,17 @@ redeyed@~1.0.0: "@redis/search" "1.1.6" "@redis/time-series" "1.0.5" +"redis-5@npm:redis@^5.12.0": + version "5.12.1" + resolved "https://registry.yarnpkg.com/redis/-/redis-5.12.1.tgz#f95297e01eca8b87a109601a2418647e05f16bee" + integrity sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g== + dependencies: + "@redis/bloom" "5.12.1" + "@redis/client" "5.12.1" + "@redis/json" "5.12.1" + "@redis/search" "5.12.1" + "@redis/time-series" "5.12.1" + redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" From 8c3ac0628059dfcee741680d6b2de37c2db79155 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Fri, 8 May 2026 10:03:19 +0200 Subject: [PATCH 02/32] feat(profiling-node): Bump `@sentry-internal/node-cpu-profiler` to 2.4.0 (#20720) Bumps this from 2.2.0 to 2.4.0, including the following changes: ## v2.4.0 * feat: Add Node 26 support (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/32) ## v2.3.0 * fix: Memory leaks, integer truncation, UB, and double-stop (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/26) * fix: Integer arithmetic for timestamps, emit elapsed_since_start_ns as string (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/28) * fix: Dead member, capacity checks, insert perf, napi_env by value (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/27) * chore: Replace execSync with execFileSync to prevent command injection (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/23) * chore: Pin GitHub Actions to full-length commit SHAs (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/24) * ci: Build Linux in container for wider glibc support (https://github.com/getsentry/sentry-javascript-profiling-node-binaries/pull/16) --- packages/profiling-node/package.json | 2 +- yarn.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index b696718bc4dc..1abf60a20e2c 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -61,7 +61,7 @@ "test:watch": "vitest --watch" }, "dependencies": { - "@sentry-internal/node-cpu-profiler": "^2.2.0", + "@sentry-internal/node-cpu-profiler": "^2.4.0", "@sentry/core": "10.52.0", "@sentry/node": "10.52.0" }, diff --git a/yarn.lock b/yarn.lock index c920dff0d60d..a0d884c0e5ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7686,10 +7686,10 @@ resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== -"@sentry-internal/node-cpu-profiler@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz#0640d4aebb4d36031658ccff83dc22b76f437ede" - integrity sha512-oLHVYurqZfADPh5hvmQYS5qx8t0UZzT2u6+/68VXsFruQEOnYJTODKgU3BVLmemRs3WE6kCJjPeFdHVYOQGSzQ== +"@sentry-internal/node-cpu-profiler@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.4.0.tgz#3f14cb7c3637b48e87056c2a5787c5e0aa90b987" + integrity sha512-zMrbqkd05LS1Ibt+js4R1aMmjdAO0yi9xiywWeulYs/bxN8P5qq20QHYleI76MorsocvYJAFo9GkYfzyzMd6Og== dependencies: detect-libc "^2.0.3" node-abi "^3.73.0" @@ -28527,7 +28527,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From 30a9c2997365d6d7807adb651cd8b62a5babc08f Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 12:37:27 +0200 Subject: [PATCH 03/32] chore(size-limit): weekly auto-bump (#20721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Size limit auto-bump | Entry | Old limit | New limit | Δ | | --- | --- | --- | --- | | @sentry/browser (incl. Tracing, Replay) - with treeshaking flags | 78 KB | 79 KB | +1 KB | | @sentry/browser (incl. sendFeedback) | 36 KB | 37 KB | +1 KB | | @sentry/react | 33 KB | 34 KB | +1 KB | | @sentry/vue (incl. Tracing) | 51 KB | 52 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) | 96 KB | 97 KB | +1 KB | | CDN Bundle (incl. Logs, Metrics) - uncompressed | 94 KB | 95 KB | +1 KB | | CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed | 218 KB | 219 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay) - uncompressed | 263 KB | 264 KB | +1 KB | | CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed | 277 KB | 278 KB | +1 KB | | @sentry/node-core | 65 KB | 66 KB | +1 KB | | @sentry/node | 176 KB | 171 KB | -5 KB | | @sentry/node - without tracing | 102 KB | 79 KB | -23 KB | | @sentry/aws-serverless | 119 KB | 113 KB | -6 KB | | @sentry/cloudflare (withSentry) - minified | 167 KiB | 171 KiB | +4 KiB | | @sentry/cloudflare (withSentry) | 420 KiB | 423 KiB | +3 KiB | --- .size-limit.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index c1be177bc5a2..2c0830fee349 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -72,7 +72,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '78 KB', + limit: '79 KB', disablePlugins: ['@size-limit/esbuild'], modifyWebpackConfig: function (config) { const webpack = require('webpack'); @@ -120,7 +120,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'sendFeedback'), gzip: true, - limit: '36 KB', + limit: '37 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -162,7 +162,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '33 KB', + limit: '34 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -188,7 +188,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '51 KB', + limit: '52 KB', disablePlugins: ['@size-limit/esbuild'], }, // Svelte SDK (ESM) @@ -261,7 +261,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)', path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: true, - limit: '96 KB', + limit: '97 KB', disablePlugins: ['@size-limit/esbuild'], }, // browser CDN bundles (non-gzipped) @@ -286,7 +286,7 @@ module.exports = [ path: createCDNPath('bundle.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '94 KB', + limit: '95 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -302,7 +302,7 @@ module.exports = [ path: createCDNPath('bundle.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '218 KB', + limit: '219 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -310,7 +310,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '263 KB', + limit: '264 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -326,7 +326,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: false, brotli: false, - limit: '277 KB', + limit: '278 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -364,7 +364,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '65 KB', + limit: '66 KB', disablePlugins: ['@size-limit/esbuild'], }, // Node SDK (ESM) @@ -374,7 +374,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '176 KB', + limit: '171 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -382,7 +382,7 @@ module.exports = [ path: 'packages/node/build/esm/index.js', import: createImport('initWithoutDefaultIntegrations', 'getDefaultIntegrationsWithoutPerformance'), gzip: true, - limit: '102 KB', + limit: '79 KB', disablePlugins: ['@size-limit/esbuild'], ignore: [...builtinModules, ...nodePrefixedBuiltinModules], modifyWebpackConfig: function (config) { @@ -406,7 +406,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '119 KB', + limit: '113 KB', disablePlugins: ['@size-limit/esbuild'], }, // Cloudflare SDK (ESM) - compressed, minified to match `wrangler deploy --dry-run --minify` output @@ -417,7 +417,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '167 KiB', + limit: '171 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { @@ -437,7 +437,7 @@ module.exports = [ ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: false, brotli: false, - limit: '420 KiB', + limit: '423 KiB', disablePlugins: ['@size-limit/webpack'], webpack: false, modifyEsbuildConfig: function (config) { From 298807727c8122925b465189440c7f5ee585c7b6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Fri, 8 May 2026 12:57:04 -0400 Subject: [PATCH 04/32] chore: bump replay dependencies (#20746) Bumps all rrweb dependencies in tests and internal packages to the latest versions. The changelog for these new releases is: - [(rrweb-snapshot) Rewrite vulnerable regexes](https://github.com/getsentry/rrweb/pull/289) - [Use CSS Declaration replaceSync to parse styles to avoid CSP violations](https://github.com/getsentry/rrweb/pull/286) - [Wrap iframe contentWindow access in try-catch](https://github.com/getsentry/rrweb/pull/275) Full changelog can be [found here](https://github.com/getsentry/rrweb/releases/tag/2.42.0). --- .../browser-integration-tests/package.json | 2 +- packages/replay-canvas/package.json | 2 +- packages/replay-internal/package.json | 4 +- yarn.lock | 76 +++++-------------- 4 files changed, 25 insertions(+), 59 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 574536fb83c5..cdab29e79ffd 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -59,7 +59,7 @@ "@babel/core": "^7.27.7", "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.56.0", - "@sentry-internal/rrweb": "2.34.0", + "@sentry-internal/rrweb": "2.42.0", "@sentry/browser": "10.52.0", "@sentry-internal/replay": "10.52.0", "@sentry/opentelemetry": "10.52.0", diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 24f77167c532..fe38f47fe2fa 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -65,7 +65,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@sentry-internal/rrweb": "2.40.0" + "@sentry-internal/rrweb": "2.42.0" }, "dependencies": { "@sentry-internal/replay": "10.52.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 721ed34acb91..12326474494f 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -79,8 +79,8 @@ "devDependencies": { "@babel/core": "^7.27.7", "@sentry-internal/replay-worker": "10.52.0", - "@sentry-internal/rrweb": "2.40.0", - "@sentry-internal/rrweb-snapshot": "2.40.0", + "@sentry-internal/rrweb": "2.42.0", + "@sentry-internal/rrweb-snapshot": "2.42.0", "fflate": "0.8.2", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.3.0" diff --git a/yarn.lock b/yarn.lock index a0d884c0e5ae..16aa3ea14f82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7702,68 +7702,34 @@ detect-libc "^2.0.4" node-abi "^3.89.0" -"@sentry-internal/rrdom@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.34.0.tgz#fccc9fe211c3995d4200abafbe8d75b671961ee9" - integrity sha512-NFGNzI9iGYpJ1D7j8qLu4pFMGDMumQzM9/wMPQpmDQTCZYV25To5lxT7z5K1huPAIyh5NLW+hQlMx/hXxXwJ6w== +"@sentry-internal/rrdom@2.42.0": + version "2.42.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.42.0.tgz#fc26d88d01edce7580b66f255b8ad65816829aaa" + integrity sha512-ecNUqhoDf64dOsGhW4/46AzNWQaAvM+xpruirOWimZE4CsXSWwM558BbIa5qsm9f5pvsnnMHzxQZM0EOf2SZ0g== dependencies: - "@sentry-internal/rrweb-snapshot" "2.34.0" + "@sentry-internal/rrweb-snapshot" "2.42.0" -"@sentry-internal/rrdom@2.40.0": - version "2.40.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.40.0.tgz#2afd76ab6695a63e7b000cb3c101029cd2ccd48b" - integrity sha512-QBnn2F0qi4Lx7TZW41CdRek/vWWLZCDx1Ywc1SimBX+byuVmNP84qvnVI4wKMoDvU6AcQiWHAgX2tGoa3Ol8pw== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.40.0" - -"@sentry-internal/rrweb-snapshot@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.34.0.tgz#79c2049b6c887e3c128d5fa80d6f745a61dd0e68" - integrity sha512-9Tb8jwVufn5GLV0d/CTuoZWo2O06ZB+xWeTJdEkbtJ6PAmO/Q7GQI3uNIx0pfFEnXP+0Km8CKKxpwkEM0z2m6w== +"@sentry-internal/rrweb-snapshot@2.42.0": + version "2.42.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.42.0.tgz#fdf3ce47f2c08601f075bdba142d3305d3435455" + integrity sha512-LB32c0hxFQbE4mNXWt31k/blPceD+9SkkyGI90mFcL6Mevca6ZEw+YejvgHUt0sM58WPRbpLPXo+U6XFBzVBIw== -"@sentry-internal/rrweb-snapshot@2.40.0": - version "2.40.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.40.0.tgz#873e9be9967d7e4b6518beb7d9dfbdf5bbe043ac" - integrity sha512-uxYlYUIiybRqcyp5go46G5lcOswTFfeen8PelYVQsiLX34I7eugNfLgFchpBdiWv1FXwsautBWyOsZlxCPc3Zw== - -"@sentry-internal/rrweb-types@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.34.0.tgz#32b853d93d1d9a1ae1888b17d84b24e674fadee0" - integrity sha512-6g5TN8YjqxrZOSQZGMLeZ2XYXdmqaKzPdIzKRySK+rKT/1fJE2gcefJEPDxiix0z/6/v5hGu/Ia8+wbJ7ACHwQ== +"@sentry-internal/rrweb-types@2.42.0": + version "2.42.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.42.0.tgz#655c86e7822f8169d108bad1261d8108d3627bbb" + integrity sha512-/+mzE1NGd5QaJy1OUqtBszHLTe5KziuKby9ULsonVEnru+0JbuJRiPA+qWLft6MfdyCcfm0Q8GYgy0H85sETbw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.34.0" + "@sentry-internal/rrweb-snapshot" "2.42.0" "@types/css-font-loading-module" "0.0.7" -"@sentry-internal/rrweb-types@2.40.0": - version "2.40.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.40.0.tgz#0c16376b83d264548f67e757dc28ae01d2e46991" - integrity sha512-d4MB1NI7KeomX0vRy0E7OQJHI+WvbeKvwAHqG/xV94A4ZscKkF7DoAQUvyeBsFf3tED/SuwV75HNEnq2uwk/cQ== - dependencies: - "@sentry-internal/rrweb-snapshot" "2.40.0" - "@types/css-font-loading-module" "0.0.7" - -"@sentry-internal/rrweb@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.34.0.tgz#a32945504f1ba2ba60f2ebd7a17d2df5e1aa010d" - integrity sha512-NAzpnMOvsIV8o6rEvJ7SDs/TwuHXSrRmuAYYedrOQyJoLq00HF+6wQGe6SAyXv/bkumXmQfjyJ6bv4XNtC4S6g== - dependencies: - "@sentry-internal/rrdom" "2.34.0" - "@sentry-internal/rrweb-snapshot" "2.34.0" - "@sentry-internal/rrweb-types" "2.34.0" - "@types/css-font-loading-module" "0.0.7" - "@xstate/fsm" "^1.4.0" - base64-arraybuffer "^1.0.1" - fflate "^0.4.4" - mitt "^3.0.0" - -"@sentry-internal/rrweb@2.40.0": - version "2.40.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.40.0.tgz#9d60899800f21f061f38b1b62ddc64df1ff525bb" - integrity sha512-niFva5QmCTfavotLvIeFSvO0rfzbJwW04igcPaWAqTDATi+Xife27iBeVMBmjpHEWygGYkBaGyBQUUi8zUdAyg== +"@sentry-internal/rrweb@2.42.0": + version "2.42.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.42.0.tgz#d66013382a1b887d2f77978cb107cf5830542864" + integrity sha512-Rh3Qpt5E6+woQ5aupT0SECUAy0cCi8eyEFVyIGUDJW7lGeX/vRy5Mv75N4uQzy+RELxH5yhwkaOK/H3Ncf0FHw== dependencies: - "@sentry-internal/rrdom" "2.40.0" - "@sentry-internal/rrweb-snapshot" "2.40.0" - "@sentry-internal/rrweb-types" "2.40.0" + "@sentry-internal/rrdom" "2.42.0" + "@sentry-internal/rrweb-snapshot" "2.42.0" + "@sentry-internal/rrweb-types" "2.42.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From a3d1202ff9254625734c2cc5948dd7d1e5efbcda Mon Sep 17 00:00:00 2001 From: Dillon Mulroy Date: Sat, 9 May 2026 18:09:05 -0400 Subject: [PATCH 05/32] fix(cloudflare): avoid flush lock self-wait (#20719) - Fix workflow final flushing to use the original/bound `waitUntil`, matching the request wrapper pattern. - Pass the workflow execution context into Cloudflare SDK initialization so workflow flushes wait for already-scheduled `waitUntil` work without scheduling the final `flushAndDispose(client)` through the same flush-locked `context.waitUntil`. - Add a regression test that models the Cloudflare Vitest worker pool draining `waitUntil` promises with a timeout. Note: this PR was written by Pi using GPT-5.5 on medium and directly guided/steered and reviewed by Dillon Mulroy . Co-authored-by: GPT-5.5 --- packages/cloudflare/src/workflows.ts | 11 ++--- packages/cloudflare/test/workflow.test.ts | 54 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index be9b363715eb..e77772c73783 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -63,9 +63,9 @@ async function propagationContextFromInstanceId(instanceId: string): Promise>( @@ -112,7 +112,7 @@ class WrappedWorkflowStep implements WorkflowStep { captureException(error, { mechanism: { handled: true, type: 'auto.faas.cloudflare.workflow' } }); throw error; } finally { - this._ctx.waitUntil(flush(2000)); + this._waitUntil(flush(2000)); } }, ); @@ -175,7 +175,8 @@ export function instrumentWorkflowWithSentry< setAsyncLocalStorageAsyncContextStrategy(); return withIsolationScope(async isolationScope => { - const client = init({ ...options, enableDedupe: false }); + const waitUntil = context.waitUntil.bind(context); + const client = init({ ...options, ctx: context, enableDedupe: false }); isolationScope.setClient(client); addCloudResourceContext(isolationScope); @@ -188,10 +189,10 @@ export function instrumentWorkflowWithSentry< return await obj.run.call( obj, event, - new WrappedWorkflowStep(event.instanceId, context, options, step), + new WrappedWorkflowStep(event.instanceId, options, step, waitUntil), ); } finally { - context.waitUntil(flushAndDispose(client)); + waitUntil(flushAndDispose(client)); } }); }); diff --git a/packages/cloudflare/test/workflow.test.ts b/packages/cloudflare/test/workflow.test.ts index 18dee2c09cfd..008486697b5b 100644 --- a/packages/cloudflare/test/workflow.test.ts +++ b/packages/cloudflare/test/workflow.test.ts @@ -74,6 +74,31 @@ const INSTANCE_ID = 'ae0ee067-61b3-4852-9219-5d62282270f0'; const SAMPLE_RAND = '0.44116884107728693'; const TRACE_ID = INSTANCE_ID.replace(/-/g, ''); +async function drainWaitUntilLikeCloudflareVitestPool( + waitUntilPromises: Promise[], + timeoutMs = 100, +): Promise { + while (waitUntilPromises.length > 0) { + const batch = waitUntilPromises.splice(0); + let timeoutId: ReturnType | undefined; + + const result = await Promise.race([ + Promise.allSettled(batch).then(() => 'settled' as const), + new Promise<'timed-out'>(resolve => { + timeoutId = setTimeout(() => resolve('timed-out'), timeoutMs); + }), + ]); + + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (result === 'timed-out') { + throw new Error('Cloudflare Vitest worker pool timed out while draining waitUntil promises'); + } + } +} + describe.skipIf(NODE_MAJOR_VERSION < 20)('workflows', () => { beforeEach(() => { vi.clearAllMocks(); @@ -152,6 +177,35 @@ describe.skipIf(NODE_MAJOR_VERSION < 20)('workflows', () => { ]); }); + test('workflow step and final flush waitUntil promises can be drained by the Cloudflare Vitest worker pool', async () => { + const waitUntilPromises: Promise[] = []; + const context: ExecutionContext = { + waitUntil: vi.fn((promise: Promise) => { + waitUntilPromises.push(promise); + }), + passThroughOnException: vi.fn(), + props: {}, + }; + + class WaitUntilWorkflow { + public constructor(private _ctx: ExecutionContext) {} + + public async run(_event: Readonly>, step: WorkflowStep): Promise { + await step.do('waitUntil step', async () => { + this._ctx.waitUntil(new Promise(resolve => setTimeout(resolve, 0))); + }); + } + } + + const TestWorkflowInstrumented = instrumentWorkflowWithSentry(getSentryOptions, WaitUntilWorkflow as any); + const workflow = new TestWorkflowInstrumented(context, {}) as WaitUntilWorkflow; + const event = { payload: {}, timestamp: new Date(), instanceId: INSTANCE_ID }; + + await workflow.run(event, mockStep); + + await expect(drainWaitUntilLikeCloudflareVitestPool(waitUntilPromises)).resolves.toBeUndefined(); + }); + test('Wraps env with instrumentEnv', async () => { class EnvTestWorkflow { constructor(_ctx: ExecutionContext, _env: unknown) {} From 80f05ff77fef579043c49d4e6576a09d13912b19 Mon Sep 17 00:00:00 2001 From: isaacs Date: Sat, 9 May 2026 16:06:50 -0700 Subject: [PATCH 06/32] feat(core): split exports by browser/server for bundle size (#20435) Split the exports from `@sentry/core` into three options: - `@sentry/core`, the default (unchanged) - `@sentry/core/browser`, containing _only_ shared and browser-specific functionality, nothing server-specific. - `@sentry/core/server`, containing _only_ shared and server-specific functionality, nothing browser-specific. This allows us to make the bundle sizes quite a bit smaller in our browser SDKs where this is important, while adding more functionality to our server-specific SDKs, in `@sentry/core` where they can be easily shared across runtimes. fix: #20434 fix: JS-2243 --- .../react-router-6/tsconfig.json | 2 +- .../react-send-to-sentry/tsconfig.json | 2 +- dev-packages/rollup-utils/npmHelpers.mjs | 26 +- dev-packages/rollup-utils/utils.mjs | 19 + packages/astro/tsconfig.json | 1 + packages/browser/src/client.ts | 4 +- packages/browser/src/diagnose-sdk.ts | 2 +- packages/browser/src/eventbuilder.ts | 4 +- packages/browser/src/exports.ts | 6 +- packages/browser/src/feedbackAsync.ts | 2 +- packages/browser/src/feedbackSync.ts | 2 +- packages/browser/src/helpers.ts | 4 +- .../browser/src/index.bundle.logs.metrics.ts | 2 +- .../src/index.bundle.replay.logs.metrics.ts | 2 +- .../src/index.bundle.tracing.logs.metrics.ts | 6 +- ...le.tracing.replay.feedback.logs.metrics.ts | 4 +- .../index.bundle.tracing.replay.feedback.ts | 4 +- ...ndex.bundle.tracing.replay.logs.metrics.ts | 6 +- .../src/index.bundle.tracing.replay.ts | 4 +- packages/browser/src/index.bundle.tracing.ts | 4 +- packages/browser/src/index.ts | 6 +- .../index.captureconsole.ts | 2 +- .../index.createlangchaincallbackhandler.ts | 2 +- .../src/integrations-bundle/index.dedupe.ts | 2 +- .../index.extraerrordata.ts | 2 +- .../src/integrations-bundle/index.feedback.ts | 2 +- .../index.instrumentanthropicaiclient.ts | 2 +- .../index.instrumentgooglegenaiclient.ts | 2 +- .../index.instrumentlangchainembeddings.ts | 2 +- .../index.instrumentlanggraph.ts | 2 +- .../index.instrumentopenaiclient.ts | 2 +- .../index.modulemetadata.ts | 2 +- .../index.rewriteframes.ts | 2 +- .../browser/src/integrations/breadcrumbs.ts | 4 +- .../src/integrations/browserapierrors.ts | 4 +- .../src/integrations/browsersession.ts | 2 +- .../browser/src/integrations/contextlines.ts | 4 +- .../src/integrations/culturecontext.ts | 4 +- .../featureFlags/growthbook/integration.ts | 4 +- .../featureFlags/launchdarkly/integration.ts | 4 +- .../featureFlags/openfeature/integration.ts | 4 +- .../featureFlags/statsig/integration.ts | 4 +- .../featureFlags/unleash/integration.ts | 4 +- .../src/integrations/globalhandlers.ts | 4 +- .../browser/src/integrations/graphqlClient.ts | 4 +- .../browser/src/integrations/httpclient.ts | 4 +- .../browser/src/integrations/httpcontext.ts | 2 +- .../browser/src/integrations/linkederrors.ts | 4 +- .../src/integrations/reportingobserver.ts | 4 +- .../browser/src/integrations/spanstreaming.ts | 4 +- .../browser/src/integrations/spotlight.ts | 4 +- .../src/integrations/view-hierarchy.ts | 4 +- .../browser/src/integrations/webWorker.ts | 4 +- .../index.multiplexedtransport.ts | 2 +- packages/browser/src/profiling/UIProfiler.ts | 4 +- packages/browser/src/profiling/index.ts | 4 +- packages/browser/src/profiling/integration.ts | 4 +- .../src/profiling/startProfileForSpan.ts | 4 +- packages/browser/src/profiling/utils.ts | 4 +- packages/browser/src/report-dialog.ts | 4 +- packages/browser/src/sdk.ts | 4 +- packages/browser/src/stack-parsers.ts | 4 +- packages/browser/src/tracing/backgroundtab.ts | 2 +- .../src/tracing/browserTracingIntegration.ts | 4 +- packages/browser/src/tracing/linkedTraces.ts | 6 +- .../browser/src/tracing/reportPageLoaded.ts | 4 +- packages/browser/src/tracing/request.ts | 4 +- packages/browser/src/tracing/setActiveSpan.ts | 4 +- packages/browser/src/transports/fetch.ts | 4 +- packages/browser/src/transports/offline.ts | 10 +- packages/browser/src/transports/types.ts | 2 +- packages/browser/src/userfeedback.ts | 4 +- .../src/utils/detectBrowserExtension.ts | 2 +- .../browser/src/utils/lazyLoadIntegration.ts | 4 +- packages/browser/test/client.test.ts | 6 +- packages/browser/test/diagnose-sdk.test.ts | 6 +- packages/browser/test/eventbuilder.test.ts | 4 +- .../test/helper/browser-client-options.ts | 2 +- packages/browser/test/helpers.test.ts | 2 +- .../test/index.bundle.logs.metrics.test.ts | 2 +- .../index.bundle.replay.logs.metrics.test.ts | 2 +- .../index.bundle.tracing.logs.metrics.test.ts | 2 +- ...acing.replay.feedback.logs.metrics.test.ts | 2 +- ...bundle.tracing.replay.logs.metrics.test.ts | 2 +- packages/browser/test/index.test.ts | 6 +- .../test/integrations/breadcrumbs.test.ts | 2 +- .../test/integrations/contextlines.test.ts | 2 +- .../test/integrations/graphqlClient.test.ts | 4 +- .../integrations/reportingobserver.test.ts | 4 +- .../test/integrations/spanstreaming.test.ts | 8 +- .../test/integrations/webWorker.test.ts | 4 +- .../browser/test/mocks/simpletransport.ts | 2 +- .../browser/test/profiling/UIProfiler.test.ts | 2 +- .../test/profiling/integration.test.ts | 2 +- packages/browser/test/sdk.test.ts | 6 +- packages/browser/test/tracekit/ie.test.ts | 2 +- packages/browser/test/tracekit/opera.test.ts | 2 +- .../test/tracing/backgroundtab.test.ts | 2 +- .../tracing/browserTracingIntegration.test.ts | 4 +- .../browser/test/tracing/linkedTraces.test.ts | 4 +- packages/browser/test/tracing/request.test.ts | 4 +- .../test/tracing/setActiveSpan.test.ts | 2 +- .../browser/test/transports/fetch.test.ts | 4 +- .../browser/test/transports/offline.test.ts | 4 +- packages/browser/tsconfig.json | 1 + packages/core/browser.js | 3 + packages/core/package.json | 24 +- packages/core/rollup.npm.config.mjs | 43 +- packages/core/server.js | 3 + packages/core/src/browser-exports.ts | 16 + packages/core/src/browser.ts | 4 + packages/core/src/index.ts | 606 +----------------- packages/core/src/server-exports.ts | 43 ++ packages/core/src/server.ts | 11 + packages/core/src/shared-exports.ts | 545 ++++++++++++++++ packages/core/tsconfig.test.json | 1 - packages/core/tsconfig.types.json | 2 + packages/effect/rollup.npm.config.mjs | 6 +- packages/ember/tsconfig.json | 2 +- packages/hono/rollup.npm.config.mjs | 2 +- .../integration-shims/src/BrowserTracing.ts | 2 +- .../integration-shims/src/ElementTiming.ts | 2 +- packages/integration-shims/src/Feedback.ts | 4 +- packages/integration-shims/src/Replay.ts | 4 +- .../integration-shims/src/SpanStreaming.ts | 2 +- .../integration-shims/src/launchDarkly.ts | 2 +- packages/integration-shims/src/logs.ts | 4 +- packages/integration-shims/tsconfig.json | 1 + packages/nextjs/package.json | 2 +- packages/nextjs/tsconfig.json | 2 + packages/nextjs/tsconfig.tsnode.json | 9 + packages/react/src/error.ts | 2 +- .../route-manifest.ts | 2 +- .../src/reactrouter-compat-utils/utils.ts | 4 +- packages/react/src/reactrouterv3.ts | 4 +- packages/react/src/redux.ts | 4 +- packages/react/src/sdk.ts | 4 +- packages/react/src/tanstackrouter.ts | 4 +- packages/react/test/profiler.test.tsx | 4 +- .../test/reactrouter-cross-usage.test.tsx | 7 +- .../reactrouter-descendant-routes.test.tsx | 7 +- packages/react/test/reactrouterv6.test.tsx | 7 +- packages/react/test/redux.test.ts | 4 +- packages/react/tsconfig.json | 2 + packages/remix/tsconfig.json | 3 +- .../test/server/errorboundary.test.tsx | 11 +- packages/solidstart/tsconfig.json | 5 +- .../sveltekit/src/server-common/handle.ts | 1 - packages/sveltekit/src/vite/svelteConfig.ts | 1 - packages/sveltekit/tsconfig.json | 2 + packages/tanstackstart-react/tsconfig.json | 5 +- packages/typescript/tsconfig.json | 1 - packages/vue/tsconfig.json | 2 + 153 files changed, 978 insertions(+), 850 deletions(-) create mode 100644 packages/core/browser.js create mode 100644 packages/core/server.js create mode 100644 packages/core/src/browser-exports.ts create mode 100644 packages/core/src/browser.ts create mode 100644 packages/core/src/server-exports.ts create mode 100644 packages/core/src/server.ts create mode 100644 packages/core/src/shared-exports.ts create mode 100644 packages/nextjs/tsconfig.tsnode.json diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-6/tsconfig.json index 4cc95dc2689a..74afe717c988 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tsconfig.json @@ -10,7 +10,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tsconfig.json index 4cc95dc2689a..74afe717c988 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tsconfig.json @@ -10,7 +10,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 6f399c1c3f59..e361b00ef2b8 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -21,7 +21,7 @@ import { makeSucrasePlugin, } from './plugins/index.mjs'; import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs'; -import { mergePlugins } from './utils.mjs'; +import { mergeExternals, mergePlugins } from './utils.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -47,6 +47,13 @@ export function makeBaseNPMConfig(options = {}) { excludeIframe: undefined, }); + const deps = [ + ...builtinModules.filter(m => !bundledBuiltins.includes(m)), + ...Object.keys(packageDotJSON.dependencies || {}), + ...Object.keys(packageDotJSON.peerDependencies || {}), + ...Object.keys(packageDotJSON.optionalDependencies || {}), + ]; + const defaultBaseConfig = { input: entrypoints, @@ -100,17 +107,20 @@ export function makeBaseNPMConfig(options = {}) { plugins: [nodeResolvePlugin, sucrasePlugin, debugBuildStatementReplacePlugin, rrwebBuildPlugin, cleanupPlugin], // don't include imported modules from outside the package in the final output - external: [ - ...builtinModules.filter(m => !bundledBuiltins.includes(m)), - ...Object.keys(packageDotJSON.dependencies || {}), - ...Object.keys(packageDotJSON.peerDependencies || {}), - ...Object.keys(packageDotJSON.optionalDependencies || {}), - ], + // also treat subpath exports (e.g. `@sentry/core/browser`) as external + external: id => { + // treat subpath exports as external if the package is external + return deps.some(dep => id === dep || id.startsWith(`${dep}/`)); + }, }; return deepMerge(defaultBaseConfig, packageSpecificConfig, { // Plugins have to be in the correct order or everything breaks, so when merging we have to manually re-order them - customMerge: key => (key === 'plugins' ? mergePlugins : undefined), + customMerge: key => { + if (key === 'plugins') return mergePlugins; + if (key === 'external') return mergeExternals; + return undefined; + }, }); } diff --git a/dev-packages/rollup-utils/utils.mjs b/dev-packages/rollup-utils/utils.mjs index b687ff9993c4..dd64e1e38c10 100644 --- a/dev-packages/rollup-utils/utils.mjs +++ b/dev-packages/rollup-utils/utils.mjs @@ -10,6 +10,25 @@ export const insertAt = (arr, index, ...insertees) => { return newArr; }; +/** + * Turn a list of module IDs into a test function + * Includes submodule exports by checking that it starts with the name + * plus a / character. The list would contain something like `'@sentry/core'` + * and we might test it against a module id like `'@sentry/core/browser'` + */ +const toFilterFunction = list => (Array.isArray(list) ? id => list.some(test => filterTest(test, id)) : list); + +const filterTest = (test, id) => (test instanceof RegExp ? test.test(id) : id === test || id.startsWith(`${test}/`)); + +/** + * Merge two external configs (function or array), returning a function that handles both. + */ +export function mergeExternals(base, specific) { + const baseFn = toFilterFunction(base); + const specificFn = toFilterFunction(specific); + return id => baseFn(id) || specificFn(id); +} + /** * Merge two arrays of plugins, making sure they're sorted in the correct order. */ diff --git a/packages/astro/tsconfig.json b/packages/astro/tsconfig.json index bf45a09f2d71..d1a9c722679f 100644 --- a/packages/astro/tsconfig.json +++ b/packages/astro/tsconfig.json @@ -4,6 +4,7 @@ "include": ["src/**/*"], "compilerOptions": { + "moduleResolution": "bundler" // package-specific options } } diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 4ffc85b07762..8d0242d82cbf 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -8,7 +8,7 @@ import type { ParameterizedString, Scope, SeverityLevel, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { _INTERNAL_flushLogsBuffer, _INTERNAL_flushMetricsBuffer, @@ -16,7 +16,7 @@ import { applySdkMetadata, Client, getSDKSource, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; diff --git a/packages/browser/src/diagnose-sdk.ts b/packages/browser/src/diagnose-sdk.ts index 0a5fdd0da05b..0be386ce6d32 100644 --- a/packages/browser/src/diagnose-sdk.ts +++ b/packages/browser/src/diagnose-sdk.ts @@ -1,4 +1,4 @@ -import { getClient, suppressTracing } from '@sentry/core'; +import { getClient, suppressTracing } from '@sentry/core/browser'; /** * A function to diagnose why the SDK might not be successfully sending data. diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index b430007b552d..76e38ec00468 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -6,7 +6,7 @@ import type { SeverityLevel, StackFrame, StackParser, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { _INTERNAL_enhanceErrorWithSentryInfo, addExceptionMechanism, @@ -22,7 +22,7 @@ import { isPlainObject, normalizeToSize, resolvedSyncPromise, -} from '@sentry/core'; +} from '@sentry/core/browser'; type Prototype = { constructor: (...args: unknown[]) => unknown }; diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 322cc0b3c8db..0ede8e8d76be 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -20,7 +20,7 @@ export type { ExclusiveEventHintOrCaptureContext, Log, LogSeverityLevel, -} from '@sentry/core'; +} from '@sentry/core/browser'; export type { BrowserOptions } from './client'; @@ -72,14 +72,14 @@ export { updateSpanName, withStreamedSpan, metrics, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { WINDOW } from './helpers'; export { BrowserClient } from './client'; diff --git a/packages/browser/src/feedbackAsync.ts b/packages/browser/src/feedbackAsync.ts index c40964bb5c27..30f31bec01d5 100644 --- a/packages/browser/src/feedbackAsync.ts +++ b/packages/browser/src/feedbackAsync.ts @@ -5,6 +5,6 @@ import { lazyLoadIntegration } from './utils/lazyLoadIntegration'; * An integration to add user feedback to your application, * while loading most of the code lazily only when it's needed. */ -export const feedbackAsyncIntegration = buildFeedbackIntegration({ +export const feedbackAsyncIntegration: ReturnType = buildFeedbackIntegration({ lazyLoadIntegration, }); diff --git a/packages/browser/src/feedbackSync.ts b/packages/browser/src/feedbackSync.ts index ede41fefb221..879d3a7a453c 100644 --- a/packages/browser/src/feedbackSync.ts +++ b/packages/browser/src/feedbackSync.ts @@ -5,7 +5,7 @@ import { } from '@sentry-internal/feedback'; /** Add a widget to capture user feedback to your application. */ -export const feedbackSyncIntegration = buildFeedbackIntegration({ +export const feedbackSyncIntegration: ReturnType = buildFeedbackIntegration({ getModalIntegration: () => feedbackModalIntegration, getScreenshotIntegration: () => feedbackScreenshotIntegration, }); diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 93c87e1d6161..09359b122450 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,4 +1,4 @@ -import type { Mechanism, WrappedFunction } from '@sentry/core'; +import type { Mechanism, WrappedFunction } from '@sentry/core/browser'; import { addExceptionMechanism, addExceptionTypeValue, @@ -9,7 +9,7 @@ import { GLOBAL_OBJ, markFunctionWrapped, withScope, -} from '@sentry/core'; +} from '@sentry/core/browser'; export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; diff --git a/packages/browser/src/index.bundle.logs.metrics.ts b/packages/browser/src/index.bundle.logs.metrics.ts index 415a56cf7cc6..5a101f8066f8 100644 --- a/packages/browser/src/index.bundle.logs.metrics.ts +++ b/packages/browser/src/index.bundle.logs.metrics.ts @@ -8,7 +8,7 @@ import { export * from './index.bundle.base'; // TODO(v11): Export metrics here once we remove it from the base bundle. -export { logger, consoleLoggingIntegration } from '@sentry/core'; +export { logger, consoleLoggingIntegration } from '@sentry/core/browser'; export { elementTimingIntegration } from '@sentry-internal/browser-utils'; diff --git a/packages/browser/src/index.bundle.replay.logs.metrics.ts b/packages/browser/src/index.bundle.replay.logs.metrics.ts index 02938d0d7063..4a3003a491bb 100644 --- a/packages/browser/src/index.bundle.replay.logs.metrics.ts +++ b/packages/browser/src/index.bundle.replay.logs.metrics.ts @@ -7,7 +7,7 @@ import { export * from './index.bundle.base'; // TODO(v11): Export metrics here once we remove it from the base bundle. -export { logger, consoleLoggingIntegration } from '@sentry/core'; +export { logger, consoleLoggingIntegration } from '@sentry/core/browser'; export { replayIntegration, getReplay } from '@sentry-internal/replay'; diff --git a/packages/browser/src/index.bundle.tracing.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.logs.metrics.ts index 19b8118a5c04..eec04b9ee335 100644 --- a/packages/browser/src/index.bundle.tracing.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.logs.metrics.ts @@ -1,4 +1,4 @@ -import { registerSpanErrorInstrumentation } from '@sentry/core'; +import { registerSpanErrorInstrumentation } from '@sentry/core/browser'; import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; registerSpanErrorInstrumentation(); @@ -6,7 +6,7 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; // TODO(v11): Export metrics here once we remove it from the base bundle. -export { logger, consoleLoggingIntegration } from '@sentry/core'; +export { logger, consoleLoggingIntegration } from '@sentry/core/browser'; export { getActiveSpan, @@ -18,7 +18,7 @@ export { startSpan, startSpanManual, withActiveSpan, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { browserTracingIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts index 5a531f6b33a9..6131da91aeb7 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts @@ -1,4 +1,4 @@ -import { registerSpanErrorInstrumentation } from '@sentry/core'; +import { registerSpanErrorInstrumentation } from '@sentry/core/browser'; import { feedbackAsyncIntegration } from './feedbackAsync'; registerSpanErrorInstrumentation(); @@ -18,7 +18,7 @@ export { withActiveSpan, logger, consoleLoggingIntegration, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { browserTracingIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 47b43d48f376..66d34bdd7831 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -1,4 +1,4 @@ -import { registerSpanErrorInstrumentation } from '@sentry/core'; +import { registerSpanErrorInstrumentation } from '@sentry/core/browser'; import { consoleLoggingIntegrationShim, elementTimingIntegrationShim, @@ -23,7 +23,7 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { browserTracingIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts index 45c7299bf436..470f8b2279c6 100644 --- a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts @@ -1,4 +1,4 @@ -import { registerSpanErrorInstrumentation } from '@sentry/core'; +import { registerSpanErrorInstrumentation } from '@sentry/core/browser'; import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; registerSpanErrorInstrumentation(); @@ -6,7 +6,7 @@ registerSpanErrorInstrumentation(); export * from './index.bundle.base'; // TODO(v11): Export metrics here once we remove it from the base bundle. -export { logger, consoleLoggingIntegration } from '@sentry/core'; +export { logger, consoleLoggingIntegration } from '@sentry/core/browser'; export { getActiveSpan, @@ -18,7 +18,7 @@ export { startSpan, startSpanManual, withActiveSpan, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { browserTracingIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 63eb9a81c24a..5c56feada691 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -1,4 +1,4 @@ -import { registerSpanErrorInstrumentation } from '@sentry/core'; +import { registerSpanErrorInstrumentation } from '@sentry/core/browser'; import { consoleLoggingIntegrationShim, elementTimingIntegrationShim, @@ -23,7 +23,7 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { browserTracingIntegration, diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index a385ad4b0792..44bcc0d2ef4a 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -1,4 +1,4 @@ -import { registerSpanErrorInstrumentation } from '@sentry/core'; +import { registerSpanErrorInstrumentation } from '@sentry/core/browser'; import { consoleLoggingIntegrationShim, elementTimingIntegrationShim, @@ -24,7 +24,7 @@ export { withActiveSpan, getSpanDescendants, setMeasurement, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { browserTracingIntegration, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 4709e6167b3c..35b65553b7d7 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -15,7 +15,7 @@ export { rewriteFramesIntegration, consoleLoggingIntegration, createConsolaReporter, -} from '@sentry/core'; +} from '@sentry/core/browser'; export { replayIntegration, getReplay } from '@sentry-internal/replay'; export type { @@ -76,8 +76,8 @@ export { createLangChainCallbackHandler, instrumentLangChainEmbeddings, logger, -} from '@sentry/core'; -export type { Span, FeatureFlagsIntegration } from '@sentry/core'; +} from '@sentry/core/browser'; +export type { Span, FeatureFlagsIntegration } from '@sentry/core/browser'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; export { spotlightBrowserIntegration } from './integrations/spotlight'; diff --git a/packages/browser/src/integrations-bundle/index.captureconsole.ts b/packages/browser/src/integrations-bundle/index.captureconsole.ts index a2187ae98798..3d25bf4edaf5 100644 --- a/packages/browser/src/integrations-bundle/index.captureconsole.ts +++ b/packages/browser/src/integrations-bundle/index.captureconsole.ts @@ -1 +1 @@ -export { captureConsoleIntegration } from '@sentry/core'; +export { captureConsoleIntegration } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.createlangchaincallbackhandler.ts b/packages/browser/src/integrations-bundle/index.createlangchaincallbackhandler.ts index 8a64eda45579..a7bdee8b6693 100644 --- a/packages/browser/src/integrations-bundle/index.createlangchaincallbackhandler.ts +++ b/packages/browser/src/integrations-bundle/index.createlangchaincallbackhandler.ts @@ -1 +1 @@ -export { createLangChainCallbackHandler } from '@sentry/core'; +export { createLangChainCallbackHandler } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.dedupe.ts b/packages/browser/src/integrations-bundle/index.dedupe.ts index 776d967c31a9..19ba1ad738d4 100644 --- a/packages/browser/src/integrations-bundle/index.dedupe.ts +++ b/packages/browser/src/integrations-bundle/index.dedupe.ts @@ -1 +1 @@ -export { dedupeIntegration } from '@sentry/core'; +export { dedupeIntegration } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.extraerrordata.ts b/packages/browser/src/integrations-bundle/index.extraerrordata.ts index 4306f9694902..7df49cf87be6 100644 --- a/packages/browser/src/integrations-bundle/index.extraerrordata.ts +++ b/packages/browser/src/integrations-bundle/index.extraerrordata.ts @@ -1 +1 @@ -export { extraErrorDataIntegration } from '@sentry/core'; +export { extraErrorDataIntegration } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.feedback.ts b/packages/browser/src/integrations-bundle/index.feedback.ts index f5c10b970690..2518eb82e776 100644 --- a/packages/browser/src/integrations-bundle/index.feedback.ts +++ b/packages/browser/src/integrations-bundle/index.feedback.ts @@ -4,4 +4,4 @@ export { getFeedback } from '@sentry-internal/feedback'; export { feedbackAsyncIntegration, feedbackAsyncIntegration as feedbackIntegration }; -export { captureFeedback } from '@sentry/core'; +export { captureFeedback } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.instrumentanthropicaiclient.ts b/packages/browser/src/integrations-bundle/index.instrumentanthropicaiclient.ts index d82909a524d8..ab7b3157953a 100644 --- a/packages/browser/src/integrations-bundle/index.instrumentanthropicaiclient.ts +++ b/packages/browser/src/integrations-bundle/index.instrumentanthropicaiclient.ts @@ -1 +1 @@ -export { instrumentAnthropicAiClient } from '@sentry/core'; +export { instrumentAnthropicAiClient } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.instrumentgooglegenaiclient.ts b/packages/browser/src/integrations-bundle/index.instrumentgooglegenaiclient.ts index ec58139c0681..9e8316dc7e43 100644 --- a/packages/browser/src/integrations-bundle/index.instrumentgooglegenaiclient.ts +++ b/packages/browser/src/integrations-bundle/index.instrumentgooglegenaiclient.ts @@ -1 +1 @@ -export { instrumentGoogleGenAIClient } from '@sentry/core'; +export { instrumentGoogleGenAIClient } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.instrumentlangchainembeddings.ts b/packages/browser/src/integrations-bundle/index.instrumentlangchainembeddings.ts index 644b8a2ef570..b8b733fc9907 100644 --- a/packages/browser/src/integrations-bundle/index.instrumentlangchainembeddings.ts +++ b/packages/browser/src/integrations-bundle/index.instrumentlangchainembeddings.ts @@ -1 +1 @@ -export { instrumentLangChainEmbeddings } from '@sentry/core'; +export { instrumentLangChainEmbeddings } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts b/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts index c7a8c0e9e591..e54333eed24a 100644 --- a/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts +++ b/packages/browser/src/integrations-bundle/index.instrumentlanggraph.ts @@ -1 +1 @@ -export { instrumentLangGraph } from '@sentry/core'; +export { instrumentLangGraph } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.instrumentopenaiclient.ts b/packages/browser/src/integrations-bundle/index.instrumentopenaiclient.ts index 5371961ff03a..813ad7b2a9fb 100644 --- a/packages/browser/src/integrations-bundle/index.instrumentopenaiclient.ts +++ b/packages/browser/src/integrations-bundle/index.instrumentopenaiclient.ts @@ -1 +1 @@ -export { instrumentOpenAiClient } from '@sentry/core'; +export { instrumentOpenAiClient } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.modulemetadata.ts b/packages/browser/src/integrations-bundle/index.modulemetadata.ts index c4f4a2b9cf75..b63da0e5a49d 100644 --- a/packages/browser/src/integrations-bundle/index.modulemetadata.ts +++ b/packages/browser/src/integrations-bundle/index.modulemetadata.ts @@ -1 +1 @@ -export { moduleMetadataIntegration } from '@sentry/core'; +export { moduleMetadataIntegration } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations-bundle/index.rewriteframes.ts b/packages/browser/src/integrations-bundle/index.rewriteframes.ts index 07ebaf6666f5..fbdc2d071d3e 100644 --- a/packages/browser/src/integrations-bundle/index.rewriteframes.ts +++ b/packages/browser/src/integrations-bundle/index.rewriteframes.ts @@ -1 +1 @@ -export { rewriteFramesIntegration } from '@sentry/core'; +export { rewriteFramesIntegration } from '@sentry/core/browser'; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index de99621bf52f..7378ffc7c377 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -14,7 +14,7 @@ import type { IntegrationFn, XhrBreadcrumbData, XhrBreadcrumbHint, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { addBreadcrumb, addConsoleInstrumentationHandler, @@ -29,7 +29,7 @@ import { parseUrl, safeJoin, severityLevelFromString, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { FetchHint, XhrHint } from '@sentry-internal/browser-utils'; import { addClickKeypressInstrumentationHandler, diff --git a/packages/browser/src/integrations/browserapierrors.ts b/packages/browser/src/integrations/browserapierrors.ts index cd32435fa5b0..0688bcf1bb76 100644 --- a/packages/browser/src/integrations/browserapierrors.ts +++ b/packages/browser/src/integrations/browserapierrors.ts @@ -1,5 +1,5 @@ -import type { IntegrationFn, WrappedFunction } from '@sentry/core'; -import { defineIntegration, fill, getFunctionName, getOriginalFunction } from '@sentry/core'; +import type { IntegrationFn, WrappedFunction } from '@sentry/core/browser'; +import { defineIntegration, fill, getFunctionName, getOriginalFunction } from '@sentry/core/browser'; import { WINDOW, wrap } from '../helpers'; // Using a comma-separated string and split for smaller bundle size vs an array literal diff --git a/packages/browser/src/integrations/browsersession.ts b/packages/browser/src/integrations/browsersession.ts index 23fdb4087af9..7d339baa42d5 100644 --- a/packages/browser/src/integrations/browsersession.ts +++ b/packages/browser/src/integrations/browsersession.ts @@ -1,4 +1,4 @@ -import { captureSession, debug, defineIntegration, getIsolationScope, startSession } from '@sentry/core'; +import { captureSession, debug, defineIntegration, getIsolationScope, startSession } from '@sentry/core/browser'; import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/integrations/contextlines.ts b/packages/browser/src/integrations/contextlines.ts index 07dae0b9dc32..70da4bac7504 100644 --- a/packages/browser/src/integrations/contextlines.ts +++ b/packages/browser/src/integrations/contextlines.ts @@ -1,5 +1,5 @@ -import type { Event, IntegrationFn, StackFrame } from '@sentry/core'; -import { addContextToFrame, defineIntegration, GLOBAL_OBJ, stripUrlQueryAndFragment } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/core/browser'; +import { addContextToFrame, defineIntegration, GLOBAL_OBJ, stripUrlQueryAndFragment } from '@sentry/core/browser'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; diff --git a/packages/browser/src/integrations/culturecontext.ts b/packages/browser/src/integrations/culturecontext.ts index cb4a1c975937..4876d5a80dd0 100644 --- a/packages/browser/src/integrations/culturecontext.ts +++ b/packages/browser/src/integrations/culturecontext.ts @@ -1,5 +1,5 @@ -import type { CultureContext, IntegrationFn } from '@sentry/core'; -import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core'; +import type { CultureContext, IntegrationFn } from '@sentry/core/browser'; +import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core/browser'; import { WINDOW } from '../helpers'; const INTEGRATION_NAME = 'CultureContext'; diff --git a/packages/browser/src/integrations/featureFlags/growthbook/integration.ts b/packages/browser/src/integrations/featureFlags/growthbook/integration.ts index 560918535cce..38ae92009063 100644 --- a/packages/browser/src/integrations/featureFlags/growthbook/integration.ts +++ b/packages/browser/src/integrations/featureFlags/growthbook/integration.ts @@ -1,5 +1,5 @@ -import type { IntegrationFn } from '@sentry/core'; -import { growthbookIntegration as coreGrowthbookIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core/browser'; +import { growthbookIntegration as coreGrowthbookIntegration } from '@sentry/core/browser'; import type { GrowthBookClass } from './types'; /** diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index 822e4b1d7f80..5283dbb7434a 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,10 +1,10 @@ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core/browser'; import { _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index 85aedbf779f9..fd4b01ad38eb 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -13,13 +13,13 @@ * OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook()); * ``` */ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core/browser'; import { _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index 9aef234045b5..94df3857b79e 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,10 +1,10 @@ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core/browser'; import { _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { FeatureGate, StatsigClient } from './types'; /** diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index c822b49f8810..b9421efe0d37 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,4 +1,4 @@ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core/browser'; import { _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, @@ -6,7 +6,7 @@ import { debug, defineIntegration, fill, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { DEBUG_BUILD } from '../../../debug-build'; import type { UnleashClient, UnleashClientClass } from './types'; diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 70b3516b63b1..00deac133889 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,4 +1,4 @@ -import type { Client, Event, IntegrationFn, Primitive, StackParser } from '@sentry/core'; +import type { Client, Event, IntegrationFn, Primitive, StackParser } from '@sentry/core/browser'; import { addGlobalErrorInstrumentationHandler, addGlobalUnhandledRejectionInstrumentationHandler, @@ -11,7 +11,7 @@ import { isString, stripDataUrlContent, UNKNOWN_FUNCTION, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { BrowserClient } from '../client'; import { DEBUG_BUILD } from '../debug-build'; import { eventFromUnknownInput } from '../eventbuilder'; diff --git a/packages/browser/src/integrations/graphqlClient.ts b/packages/browser/src/integrations/graphqlClient.ts index 51a3fe939f23..74b1b425aaa3 100644 --- a/packages/browser/src/integrations/graphqlClient.ts +++ b/packages/browser/src/integrations/graphqlClient.ts @@ -1,4 +1,4 @@ -import type { Client, IntegrationFn } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/core/browser'; import { defineIntegration, isString, @@ -7,7 +7,7 @@ import { SEMANTIC_ATTRIBUTE_URL_FULL, spanToJSON, stringMatchesSomePattern, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { FetchHint, XhrHint } from '@sentry-internal/browser-utils'; import { getBodyString, getFetchRequestArgBody, SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts index 76f32158f496..c2152f0ab78d 100644 --- a/packages/browser/src/integrations/httpclient.ts +++ b/packages/browser/src/integrations/httpclient.ts @@ -1,4 +1,4 @@ -import type { Client, Event as SentryEvent, IntegrationFn, SentryWrappedXMLHttpRequest } from '@sentry/core'; +import type { Client, Event as SentryEvent, IntegrationFn, SentryWrappedXMLHttpRequest } from '@sentry/core/browser'; import { addExceptionMechanism, addFetchInstrumentationHandler, @@ -9,7 +9,7 @@ import { GLOBAL_OBJ, isSentryRequestUrl, supportsNativeFetch, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index bc331ed1e0f8..3a7623496a65 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,4 +1,4 @@ -import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core'; +import { defineIntegration, safeSetSpanJSONAttributes } from '@sentry/core/browser'; import { getHttpRequestData, WINDOW } from '../helpers'; /** diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index d0eace11aadb..64b7f5a3edbd 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,5 +1,5 @@ -import type { IntegrationFn } from '@sentry/core'; -import { applyAggregateErrorsToEvent, defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core/browser'; +import { applyAggregateErrorsToEvent, defineIntegration } from '@sentry/core/browser'; import { exceptionFromError } from '../eventbuilder'; interface LinkedErrorsOptions { diff --git a/packages/browser/src/integrations/reportingobserver.ts b/packages/browser/src/integrations/reportingobserver.ts index 571dca9db839..6e06e15646c0 100644 --- a/packages/browser/src/integrations/reportingobserver.ts +++ b/packages/browser/src/integrations/reportingobserver.ts @@ -1,4 +1,4 @@ -import type { Client, IntegrationFn } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/core/browser'; import { captureMessage, defineIntegration, @@ -6,7 +6,7 @@ import { GLOBAL_OBJ, supportsReportingObserver, withScope, -} from '@sentry/core'; +} from '@sentry/core/browser'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; diff --git a/packages/browser/src/integrations/spanstreaming.ts b/packages/browser/src/integrations/spanstreaming.ts index ad6a35d1813b..9f82904419c6 100644 --- a/packages/browser/src/integrations/spanstreaming.ts +++ b/packages/browser/src/integrations/spanstreaming.ts @@ -1,4 +1,4 @@ -import type { IntegrationFn } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core/browser'; import { captureSpan, debug, @@ -7,7 +7,7 @@ import { isStreamedBeforeSendSpanCallback, SpanBuffer, spanIsSampled, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; export const spanStreamingIntegration = defineIntegration(() => { diff --git a/packages/browser/src/integrations/spotlight.ts b/packages/browser/src/integrations/spotlight.ts index 4c04b16ed63b..629710e6154f 100644 --- a/packages/browser/src/integrations/spotlight.ts +++ b/packages/browser/src/integrations/spotlight.ts @@ -1,5 +1,5 @@ -import type { Client, Envelope, IntegrationFn } from '@sentry/core'; -import { debug, defineIntegration, serializeEnvelope } from '@sentry/core'; +import type { Client, Envelope, IntegrationFn } from '@sentry/core/browser'; +import { debug, defineIntegration, serializeEnvelope } from '@sentry/core/browser'; import { getNativeImplementation } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; import type { WINDOW } from '../helpers'; diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index fa35ad7e00a2..edb959eca3f9 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -1,5 +1,5 @@ -import type { Attachment, Event, EventHint, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core'; -import { defineIntegration, getComponentName } from '@sentry/core'; +import type { Attachment, Event, EventHint, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core/browser'; +import { defineIntegration, getComponentName } from '@sentry/core/browser'; import { WINDOW } from '../helpers'; interface OnElementArgs { diff --git a/packages/browser/src/integrations/webWorker.ts b/packages/browser/src/integrations/webWorker.ts index 147135526ec3..a79dc463278a 100644 --- a/packages/browser/src/integrations/webWorker.ts +++ b/packages/browser/src/integrations/webWorker.ts @@ -1,5 +1,5 @@ -import type { DebugImage, Integration, IntegrationFn } from '@sentry/core'; -import { captureEvent, debug, defineIntegration, getClient, isPlainObject, isPrimitive } from '@sentry/core'; +import type { DebugImage, Integration, IntegrationFn } from '@sentry/core/browser'; +import { captureEvent, debug, defineIntegration, getClient, isPlainObject, isPrimitive } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import { eventFromUnknownInput } from '../eventbuilder'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts index a7d637d9e62f..a0b137cb1a53 100644 --- a/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts +++ b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts @@ -1 +1 @@ -export { makeMultiplexedTransport } from '@sentry/core'; +export { makeMultiplexedTransport } from '@sentry/core/browser'; diff --git a/packages/browser/src/profiling/UIProfiler.ts b/packages/browser/src/profiling/UIProfiler.ts index 932b442a4b6e..aa9c6a5f5fdf 100644 --- a/packages/browser/src/profiling/UIProfiler.ts +++ b/packages/browser/src/profiling/UIProfiler.ts @@ -1,4 +1,4 @@ -import type { Client, ContinuousProfiler, ProfileChunk, ProfileChunkEnvelope, Span } from '@sentry/core'; +import type { Client, ContinuousProfiler, ProfileChunk, ProfileChunkEnvelope, Span } from '@sentry/core/browser'; import { createEnvelope, debug, @@ -7,7 +7,7 @@ import { getRootSpan, getSdkMetadataForEnvelopeHeader, uuid4, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { BrowserOptions } from '../client'; import { DEBUG_BUILD } from './../debug-build'; import type { JSSelfProfiler } from './jsSelfProfiling'; diff --git a/packages/browser/src/profiling/index.ts b/packages/browser/src/profiling/index.ts index 5847c070dd48..3f1fe9a82ed4 100644 --- a/packages/browser/src/profiling/index.ts +++ b/packages/browser/src/profiling/index.ts @@ -1,5 +1,5 @@ -import type { Profiler } from '@sentry/core'; -import { debug, getClient } from '@sentry/core'; +import type { Profiler } from '@sentry/core/browser'; +import { debug, getClient } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; /** diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 84cd33588320..22706117fa74 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,5 +1,5 @@ -import type { EventEnvelope, IntegrationFn, Profile, Span } from '@sentry/core'; -import { debug, defineIntegration, getActiveSpan, getRootSpan, hasSpansEnabled } from '@sentry/core'; +import type { EventEnvelope, IntegrationFn, Profile, Span } from '@sentry/core/browser'; +import { debug, defineIntegration, getActiveSpan, getRootSpan, hasSpansEnabled } from '@sentry/core/browser'; import type { BrowserOptions } from '../client'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/profiling/startProfileForSpan.ts b/packages/browser/src/profiling/startProfileForSpan.ts index 6eaaa016d822..6c36c6bf84aa 100644 --- a/packages/browser/src/profiling/startProfileForSpan.ts +++ b/packages/browser/src/profiling/startProfileForSpan.ts @@ -1,5 +1,5 @@ -import type { Span } from '@sentry/core'; -import { debug, getCurrentScope, spanToJSON, timestampInSeconds, uuid4 } from '@sentry/core'; +import type { Span } from '@sentry/core/browser'; +import { debug, getCurrentScope, spanToJSON, timestampInSeconds, uuid4 } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import type { JSSelfProfile } from './jsSelfProfiling'; diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index f0d067c841d8..12446e6fb986 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -10,7 +10,7 @@ import type { ProfileChunk, Span, ThreadCpuProfile, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { browserPerformanceTimeOrigin, debug, @@ -22,7 +22,7 @@ import { spanToJSON, timestampInSeconds, uuid4, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { BrowserOptions } from '../client'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/report-dialog.ts b/packages/browser/src/report-dialog.ts index 03255a7db91d..beb6f7ec9c08 100644 --- a/packages/browser/src/report-dialog.ts +++ b/packages/browser/src/report-dialog.ts @@ -1,5 +1,5 @@ -import type { ReportDialogOptions } from '@sentry/core'; -import { debug, getClient, getCurrentScope, getReportDialogEndpoint, lastEventId } from '@sentry/core'; +import type { ReportDialogOptions } from '@sentry/core/browser'; +import { debug, getClient, getCurrentScope, getReportDialogEndpoint, lastEventId } from '@sentry/core/browser'; import { DEBUG_BUILD } from './debug-build'; import { WINDOW } from './helpers'; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 8a457f858f3c..68a48ec461d1 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,4 +1,4 @@ -import type { Client, Integration, Options } from '@sentry/core'; +import type { Client, Integration, Options } from '@sentry/core/browser'; import { conversationIdIntegration, dedupeIntegration, @@ -7,7 +7,7 @@ import { inboundFiltersIntegration, initAndBind, stackParserFromStackParserOptions, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { BrowserClientOptions, BrowserOptions } from './client'; import { BrowserClient } from './client'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index cb74bc1e6ce6..9214dac42707 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -23,8 +23,8 @@ // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import type { StackFrame, StackLineParser, StackLineParserFn } from '@sentry/core'; -import { createStackParser, UNKNOWN_FUNCTION } from '@sentry/core'; +import type { StackFrame, StackLineParser, StackLineParserFn } from '@sentry/core/browser'; +import { createStackParser, UNKNOWN_FUNCTION } from '@sentry/core/browser'; const OPERA10_PRIORITY = 10; const OPERA11_PRIORITY = 20; diff --git a/packages/browser/src/tracing/backgroundtab.ts b/packages/browser/src/tracing/backgroundtab.ts index f8aeca761d85..c484bc0e52c2 100644 --- a/packages/browser/src/tracing/backgroundtab.ts +++ b/packages/browser/src/tracing/backgroundtab.ts @@ -1,4 +1,4 @@ -import { debug, getActiveSpan, getRootSpan, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core'; +import { debug, getActiveSpan, getRootSpan, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 34a9609099ad..08acf99761fc 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -7,7 +7,7 @@ import type { Span, StartSpanOptions, TransactionSource, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { addNonEnumerableProperty, browserPerformanceTimeOrigin, @@ -36,7 +36,7 @@ import { startInactiveSpan, timestampInSeconds, TRACING_DEFAULTS, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { addHistoryInstrumentationHandler, addPerformanceEntries, diff --git a/packages/browser/src/tracing/linkedTraces.ts b/packages/browser/src/tracing/linkedTraces.ts index 4768ac7c66a1..8b13ff99fb35 100644 --- a/packages/browser/src/tracing/linkedTraces.ts +++ b/packages/browser/src/tracing/linkedTraces.ts @@ -1,4 +1,4 @@ -import type { Client, PropagationContext, Span, SpanContextData } from '@sentry/core'; +import type { Client, PropagationContext, Span, SpanContextData } from '@sentry/core/browser'; import { debug, getCurrentScope, @@ -7,7 +7,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, spanToJSON, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../exports'; @@ -231,7 +231,7 @@ export function getPreviousTraceFromSessionStorage(): PreviousTraceInfo | undefi } /** - * see {@link import('@sentry/core').spanIsSampled} + * see {@link import('@sentry/core/browser').spanIsSampled} */ export function spanContextSampled(ctx: SpanContextData): boolean { return ctx.traceFlags === 0x1; diff --git a/packages/browser/src/tracing/reportPageLoaded.ts b/packages/browser/src/tracing/reportPageLoaded.ts index 2d3d4a4991a4..fbd5e3a49933 100644 --- a/packages/browser/src/tracing/reportPageLoaded.ts +++ b/packages/browser/src/tracing/reportPageLoaded.ts @@ -1,5 +1,5 @@ -import type { Client } from '@sentry/core'; -import { getClient } from '@sentry/core'; +import type { Client } from '@sentry/core/browser'; +import { getClient } from '@sentry/core/browser'; /** * Manually report the end of the page load, resulting in the SDK ending the pageload span. diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 9cbf45563f0b..a1a281bb54dc 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -7,7 +7,7 @@ import type { SentryWrappedXMLHttpRequest, Span, SpanTimeInput, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler, @@ -29,7 +29,7 @@ import { stripDataUrlContent, stripUrlQueryAndFragment, timestampInSeconds, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { XhrHint } from '@sentry-internal/browser-utils'; import { addPerformanceInstrumentationHandler, diff --git a/packages/browser/src/tracing/setActiveSpan.ts b/packages/browser/src/tracing/setActiveSpan.ts index 5e3b537d4b6d..be1cb52adc06 100644 --- a/packages/browser/src/tracing/setActiveSpan.ts +++ b/packages/browser/src/tracing/setActiveSpan.ts @@ -1,5 +1,5 @@ -import type { Span } from '@sentry/core'; -import { _INTERNAL_setSpanForScope, getActiveSpan, getCurrentScope } from '@sentry/core'; +import type { Span } from '@sentry/core/browser'; +import { _INTERNAL_setSpanForScope, getActiveSpan, getCurrentScope } from '@sentry/core/browser'; /** * Sets an inactive span active on the current scope. diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index f6683c7005de..f5b3f2d780e8 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,5 +1,5 @@ -import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core'; -import { createTransport, makePromiseBuffer } from '@sentry/core'; +import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core/browser'; +import { createTransport, makePromiseBuffer } from '@sentry/core/browser'; import { clearCachedImplementation, getNativeImplementation } from '@sentry-internal/browser-utils'; import type { WINDOW } from '../helpers'; import type { BrowserTransportOptions } from './types'; diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts index c644e72fbf39..c965e3aa5c15 100644 --- a/packages/browser/src/transports/offline.ts +++ b/packages/browser/src/transports/offline.ts @@ -1,5 +1,11 @@ -import type { BaseTransportOptions, Envelope, OfflineStore, OfflineTransportOptions, Transport } from '@sentry/core'; -import { makeOfflineTransport, parseEnvelope, serializeEnvelope } from '@sentry/core'; +import type { + BaseTransportOptions, + Envelope, + OfflineStore, + OfflineTransportOptions, + Transport, +} from '@sentry/core/browser'; +import { makeOfflineTransport, parseEnvelope, serializeEnvelope } from '@sentry/core/browser'; import { WINDOW } from '../helpers'; import { makeFetchTransport } from './fetch'; diff --git a/packages/browser/src/transports/types.ts b/packages/browser/src/transports/types.ts index a304e9f93d66..e2a00e9a12d8 100644 --- a/packages/browser/src/transports/types.ts +++ b/packages/browser/src/transports/types.ts @@ -1,4 +1,4 @@ -import type { BaseTransportOptions } from '@sentry/core'; +import type { BaseTransportOptions } from '@sentry/core/browser'; export interface BrowserTransportOptions extends BaseTransportOptions { /** Fetch API init parameters. Used by the FetchTransport */ diff --git a/packages/browser/src/userfeedback.ts b/packages/browser/src/userfeedback.ts index bc6fd248b480..f77699dd1c75 100644 --- a/packages/browser/src/userfeedback.ts +++ b/packages/browser/src/userfeedback.ts @@ -1,5 +1,5 @@ -import type { DsnComponents, EventEnvelope, SdkMetadata, UserFeedback, UserFeedbackItem } from '@sentry/core'; -import { createEnvelope, dsnToString } from '@sentry/core'; +import type { DsnComponents, EventEnvelope, SdkMetadata, UserFeedback, UserFeedbackItem } from '@sentry/core/browser'; +import { createEnvelope, dsnToString } from '@sentry/core/browser'; /** * Creates an envelope from a user feedback. diff --git a/packages/browser/src/utils/detectBrowserExtension.ts b/packages/browser/src/utils/detectBrowserExtension.ts index 95ad7cebcf06..86ba33ba7727 100644 --- a/packages/browser/src/utils/detectBrowserExtension.ts +++ b/packages/browser/src/utils/detectBrowserExtension.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, getLocationHref } from '@sentry/core'; +import { consoleSandbox, getLocationHref } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index a24da592faff..f348681adba5 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -1,5 +1,5 @@ -import type { IntegrationFn } from '@sentry/core'; -import { getClient, SDK_VERSION } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core/browser'; +import { getClient, SDK_VERSION } from '@sentry/core/browser'; import type { BrowserClient } from '../client'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index 27135b4fc9a9..9861c9e428ec 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -2,14 +2,14 @@ * @vitest-environment jsdom */ -import * as sentryCore from '@sentry/core'; -import { Scope } from '@sentry/core'; +import * as sentryCore from '@sentry/core/browser'; +import { Scope } from '@sentry/core/browser'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; -vi.mock('@sentry/core', async requireActual => { +vi.mock('@sentry/core/browser', async requireActual => { return { ...((await requireActual()) as any), _INTERNAL_flushLogsBuffer: vi.fn(), diff --git a/packages/browser/test/diagnose-sdk.test.ts b/packages/browser/test/diagnose-sdk.test.ts index 85b60047361e..6161891424b2 100644 --- a/packages/browser/test/diagnose-sdk.test.ts +++ b/packages/browser/test/diagnose-sdk.test.ts @@ -2,13 +2,13 @@ * @vitest-environment jsdom */ -import type { Client } from '@sentry/core'; -import * as sentryCore from '@sentry/core'; +import type { Client } from '@sentry/core/browser'; +import * as sentryCore from '@sentry/core/browser'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { diagnoseSdkConnectivity } from '../src/diagnose-sdk'; // Mock the @sentry/core module -vi.mock('@sentry/core', async requireActual => { +vi.mock('@sentry/core/browser', async requireActual => { return { ...((await requireActual()) as any), getClient: vi.fn(), diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index ef233ed58a1f..bc99c9c95e49 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -2,12 +2,12 @@ * @vitest-environment jsdom */ -import { addNonEnumerableProperty } from '@sentry/core'; +import { addNonEnumerableProperty } from '@sentry/core/browser'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { defaultStackParser } from '../src'; import { eventFromMessage, eventFromUnknownInput, extractMessage, extractType } from '../src/eventbuilder'; -vi.mock('@sentry/core', async requireActual => { +vi.mock('@sentry/core/browser', async requireActual => { return { ...((await requireActual()) as any), getClient() { diff --git a/packages/browser/test/helper/browser-client-options.ts b/packages/browser/test/helper/browser-client-options.ts index 237f3cec0e05..825bfb47a672 100644 --- a/packages/browser/test/helper/browser-client-options.ts +++ b/packages/browser/test/helper/browser-client-options.ts @@ -1,4 +1,4 @@ -import { createTransport, resolvedSyncPromise } from '@sentry/core'; +import { createTransport, resolvedSyncPromise } from '@sentry/core/browser'; import type { BrowserClientOptions } from '../../src/client'; export function getDefaultBrowserClientOptions(options: Partial = {}): BrowserClientOptions { diff --git a/packages/browser/test/helpers.test.ts b/packages/browser/test/helpers.test.ts index 3031ceae19aa..9a14db35c872 100644 --- a/packages/browser/test/helpers.test.ts +++ b/packages/browser/test/helpers.test.ts @@ -1,4 +1,4 @@ -import type { WrappedFunction } from '@sentry/core'; +import type { WrappedFunction } from '@sentry/core/browser'; import { describe, expect, it, vi } from 'vitest'; import { wrap } from '../src/helpers'; diff --git a/packages/browser/test/index.bundle.logs.metrics.test.ts b/packages/browser/test/index.bundle.logs.metrics.test.ts index 7d450dc1ced0..31d4254b9433 100644 --- a/packages/browser/test/index.bundle.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.logs.metrics.test.ts @@ -1,4 +1,4 @@ -import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core'; +import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { spanStreamingIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; import * as LogsMetricsBundle from '../src/index.bundle.logs.metrics'; diff --git a/packages/browser/test/index.bundle.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.replay.logs.metrics.test.ts index d6bb995fae09..5e39b4111b63 100644 --- a/packages/browser/test/index.bundle.replay.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.replay.logs.metrics.test.ts @@ -1,4 +1,4 @@ -import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core'; +import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { browserTracingIntegrationShim, feedbackIntegrationShim, diff --git a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts index 483a4ae8a1f5..65cc58fbf028 100644 --- a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts @@ -1,4 +1,4 @@ -import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core'; +import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; import { browserTracingIntegration, spanStreamingIntegration } from '../src'; diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts index 0c474b195bc8..e4b88fab24d7 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts @@ -1,4 +1,4 @@ -import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core'; +import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { describe, expect, it } from 'vitest'; import { browserTracingIntegration, diff --git a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts index 4848de24caea..f8571872ba95 100644 --- a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts @@ -1,4 +1,4 @@ -import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core'; +import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; import { browserTracingIntegration, replayIntegration, spanStreamingIntegration } from '../src'; diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index 9a30596832e4..b662352bf3e3 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -9,8 +9,8 @@ import { getReportDialogEndpoint, lastEventId, SDK_VERSION, -} from '@sentry/core'; -import * as utils from '@sentry/core'; +} from '@sentry/core/browser'; +import * as utils from '@sentry/core/browser'; import type { Mock } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -37,7 +37,7 @@ const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; // eslint-disable-next-line no-var declare var global: any; -vi.mock('@sentry/core', async requireActual => { +vi.mock('@sentry/core/browser', async requireActual => { return { ...((await requireActual()) as any), getReportDialogEndpoint: vi.fn(), diff --git a/packages/browser/test/integrations/breadcrumbs.test.ts b/packages/browser/test/integrations/breadcrumbs.test.ts index 56df65c3a6ae..3ee91da9be13 100644 --- a/packages/browser/test/integrations/breadcrumbs.test.ts +++ b/packages/browser/test/integrations/breadcrumbs.test.ts @@ -1,4 +1,4 @@ -import * as SentryCore from '@sentry/core'; +import * as SentryCore from '@sentry/core/browser'; import { describe, expect, it, vi } from 'vitest'; import { breadcrumbsIntegration, BrowserClient, flush } from '../../src'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; diff --git a/packages/browser/test/integrations/contextlines.test.ts b/packages/browser/test/integrations/contextlines.test.ts index e34479cd644e..ffb062759d9c 100644 --- a/packages/browser/test/integrations/contextlines.test.ts +++ b/packages/browser/test/integrations/contextlines.test.ts @@ -1,4 +1,4 @@ -import type { StackFrame } from '@sentry/core'; +import type { StackFrame } from '@sentry/core/browser'; import { describe, expect, it } from 'vitest'; import { applySourceContextToFrame } from '../../src/integrations/contextlines'; diff --git a/packages/browser/test/integrations/graphqlClient.test.ts b/packages/browser/test/integrations/graphqlClient.test.ts index 1c4ab60d30f2..0ff1fb43161f 100644 --- a/packages/browser/test/integrations/graphqlClient.test.ts +++ b/packages/browser/test/integrations/graphqlClient.test.ts @@ -2,8 +2,8 @@ * @vitest-environment jsdom */ -import type { Client } from '@sentry/core'; -import { SentrySpan, spanToJSON } from '@sentry/core'; +import type { Client } from '@sentry/core/browser'; +import { SentrySpan, spanToJSON } from '@sentry/core/browser'; import type { FetchHint, XhrHint } from '@sentry-internal/browser-utils'; import { SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; import { describe, expect, test } from 'vitest'; diff --git a/packages/browser/test/integrations/reportingobserver.test.ts b/packages/browser/test/integrations/reportingobserver.test.ts index 4376977a1b1a..18ef38b7a097 100644 --- a/packages/browser/test/integrations/reportingobserver.test.ts +++ b/packages/browser/test/integrations/reportingobserver.test.ts @@ -1,5 +1,5 @@ -import type { Client } from '@sentry/core'; -import * as SentryCore from '@sentry/core'; +import type { Client } from '@sentry/core/browser'; +import * as SentryCore from '@sentry/core/browser'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { reportingObserverIntegration } from '../../src/integrations/reportingobserver'; diff --git a/packages/browser/test/integrations/spanstreaming.test.ts b/packages/browser/test/integrations/spanstreaming.test.ts index 1d5d587290a3..cc51ef6e6c17 100644 --- a/packages/browser/test/integrations/spanstreaming.test.ts +++ b/packages/browser/test/integrations/spanstreaming.test.ts @@ -1,5 +1,5 @@ -import * as SentryCore from '@sentry/core'; -import { debug } from '@sentry/core'; +import * as SentryCore from '@sentry/core/browser'; +import { debug } from '@sentry/core/browser'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient, spanStreamingIntegration } from '../../src'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; @@ -15,8 +15,8 @@ const MockSpanBuffer = vi.hoisted(() => { return vi.fn(() => mockSpanBufferInstance); }); -vi.mock('@sentry/core', async () => { - const original = await vi.importActual('@sentry/core'); +vi.mock('@sentry/core/browser', async () => { + const original = await vi.importActual('@sentry/core/browser'); return { ...original, SpanBuffer: MockSpanBuffer, diff --git a/packages/browser/test/integrations/webWorker.test.ts b/packages/browser/test/integrations/webWorker.test.ts index c239e31bd638..e824486456da 100644 --- a/packages/browser/test/integrations/webWorker.test.ts +++ b/packages/browser/test/integrations/webWorker.test.ts @@ -2,13 +2,13 @@ * @vitest-environment jsdom */ -import * as SentryCore from '@sentry/core'; +import * as SentryCore from '@sentry/core/browser'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as helpers from '../../src/helpers'; import { INTEGRATION_NAME, registerWebWorker, webWorkerIntegration } from '../../src/integrations/webWorker'; // Mock @sentry/core -vi.mock('@sentry/core', async importActual => { +vi.mock('@sentry/core/browser', async importActual => { return { ...((await importActual()) as any), debug: { diff --git a/packages/browser/test/mocks/simpletransport.ts b/packages/browser/test/mocks/simpletransport.ts index 515b0f9c0865..9f2f527fba7d 100644 --- a/packages/browser/test/mocks/simpletransport.ts +++ b/packages/browser/test/mocks/simpletransport.ts @@ -1,4 +1,4 @@ -import { createTransport, resolvedSyncPromise } from '@sentry/core'; +import { createTransport, resolvedSyncPromise } from '@sentry/core/browser'; export function makeSimpleTransport() { return createTransport({ recordDroppedEvent: () => undefined }, () => resolvedSyncPromise({})); diff --git a/packages/browser/test/profiling/UIProfiler.test.ts b/packages/browser/test/profiling/UIProfiler.test.ts index 456c5c222b22..d1702cfcbd49 100644 --- a/packages/browser/test/profiling/UIProfiler.test.ts +++ b/packages/browser/test/profiling/UIProfiler.test.ts @@ -3,7 +3,7 @@ */ import * as Sentry from '@sentry/browser'; -import { debug, type Span } from '@sentry/core'; +import { debug, type Span } from '@sentry/core/browser'; import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; import type { BrowserOptions } from '../../src/index'; diff --git a/packages/browser/test/profiling/integration.test.ts b/packages/browser/test/profiling/integration.test.ts index a08db412ccec..9ec370bc827f 100644 --- a/packages/browser/test/profiling/integration.test.ts +++ b/packages/browser/test/profiling/integration.test.ts @@ -3,7 +3,7 @@ */ import * as Sentry from '@sentry/browser'; -import { debug } from '@sentry/core'; +import { debug } from '@sentry/core/browser'; import { describe, expect, it, vi } from 'vitest'; import type { BrowserClient } from '../../src/index'; import type { JSSelfProfile } from '../../src/profiling/jsSelfProfiling'; diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index b7972797182f..3d84da69e565 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -3,9 +3,9 @@ */ /* eslint-disable @typescript-eslint/unbound-method */ -import type { Integration } from '@sentry/core'; -import * as SentryCore from '@sentry/core'; -import { createTransport, resolvedSyncPromise } from '@sentry/core'; +import type { Integration } from '@sentry/core/browser'; +import * as SentryCore from '@sentry/core/browser'; +import { createTransport, resolvedSyncPromise } from '@sentry/core/browser'; import type { Mock } from 'vitest'; import { afterEach, describe, expect, it, test, vi } from 'vitest'; import type { BrowserOptions } from '../src'; diff --git a/packages/browser/test/tracekit/ie.test.ts b/packages/browser/test/tracekit/ie.test.ts index 7bf463d39f33..ad2328ac6e26 100644 --- a/packages/browser/test/tracekit/ie.test.ts +++ b/packages/browser/test/tracekit/ie.test.ts @@ -1,4 +1,4 @@ -import { createStackParser } from '@sentry/core'; +import { createStackParser } from '@sentry/core/browser'; import { describe, expect, it } from 'vitest'; import { exceptionFromError } from '../../src/eventbuilder'; import { chromeStackLineParser, geckoStackLineParser, winjsStackLineParser } from '../../src/stack-parsers'; diff --git a/packages/browser/test/tracekit/opera.test.ts b/packages/browser/test/tracekit/opera.test.ts index d109fb39ac9c..5c85ebe8efec 100644 --- a/packages/browser/test/tracekit/opera.test.ts +++ b/packages/browser/test/tracekit/opera.test.ts @@ -1,4 +1,4 @@ -import { createStackParser } from '@sentry/core'; +import { createStackParser } from '@sentry/core/browser'; import { describe, expect, it } from 'vitest'; import { exceptionFromError } from '../../src/eventbuilder'; import { defaultStackParser, opera10StackLineParser, opera11StackLineParser } from '../../src/stack-parsers'; diff --git a/packages/browser/test/tracing/backgroundtab.test.ts b/packages/browser/test/tracing/backgroundtab.test.ts index 8d76e895848f..44e827fc9e64 100644 --- a/packages/browser/test/tracing/backgroundtab.test.ts +++ b/packages/browser/test/tracing/backgroundtab.test.ts @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import { getCurrentScope, setCurrentClient } from '@sentry/core'; +import { getCurrentScope, setCurrentClient } from '@sentry/core/browser'; import { JSDOM } from 'jsdom'; import { TextDecoder, TextEncoder } from 'util'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 83f00a09092a..59c79d98bb73 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import type { Span, StartSpanOptions } from '@sentry/core'; +import type { Span, StartSpanOptions } from '@sentry/core/browser'; import { getActiveSpan, getCurrentScope, @@ -19,7 +19,7 @@ import { TRACING_DEFAULTS, browserPerformanceTimeOrigin, getSpanDescendants, -} from '@sentry/core'; +} from '@sentry/core/browser'; import { JSDOM } from 'jsdom'; import { TextDecoder, TextEncoder } from 'util'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/packages/browser/test/tracing/linkedTraces.test.ts b/packages/browser/test/tracing/linkedTraces.test.ts index 56616c6a7692..9426a67dec08 100644 --- a/packages/browser/test/tracing/linkedTraces.test.ts +++ b/packages/browser/test/tracing/linkedTraces.test.ts @@ -1,5 +1,5 @@ -import type { Span } from '@sentry/core'; -import { addChildSpanToSpan, debug, SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core'; +import type { Span } from '@sentry/core/browser'; +import { addChildSpanToSpan, debug, SentrySpan, spanToJSON, timestampInSeconds } from '@sentry/core/browser'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { BrowserClient } from '../../src'; import type { PreviousTraceInfo } from '../../src/tracing/linkedTraces'; diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index 1674a96d1937..100f1aa9ab30 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -1,5 +1,5 @@ -import type { Client } from '@sentry/core'; -import * as utils from '@sentry/core'; +import type { Client } from '@sentry/core/browser'; +import * as utils from '@sentry/core/browser'; import * as browserUtils from '@sentry-internal/browser-utils'; import type { MockInstance } from 'vitest'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/packages/browser/test/tracing/setActiveSpan.test.ts b/packages/browser/test/tracing/setActiveSpan.test.ts index d3c7ea79cf67..ed9926a01c62 100644 --- a/packages/browser/test/tracing/setActiveSpan.test.ts +++ b/packages/browser/test/tracing/setActiveSpan.test.ts @@ -1,4 +1,4 @@ -import { getActiveSpan, SentrySpan } from '@sentry/core'; +import { getActiveSpan, SentrySpan } from '@sentry/core/browser'; import { describe, expect, it } from 'vitest'; import { setActiveSpanInBrowser } from '../../src'; diff --git a/packages/browser/test/transports/fetch.test.ts b/packages/browser/test/transports/fetch.test.ts index d330fe886d5f..5461bdd9d6f9 100644 --- a/packages/browser/test/transports/fetch.test.ts +++ b/packages/browser/test/transports/fetch.test.ts @@ -1,5 +1,5 @@ -import type { EventEnvelope, EventItem } from '@sentry/core'; -import { createEnvelope, serializeEnvelope } from '@sentry/core'; +import type { EventEnvelope, EventItem } from '@sentry/core/browser'; +import { createEnvelope, serializeEnvelope } from '@sentry/core/browser'; import type { Mock } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { makeFetchTransport } from '../../src/transports/fetch'; diff --git a/packages/browser/test/transports/offline.test.ts b/packages/browser/test/transports/offline.test.ts index 993d4977ac21..f007c553f1af 100644 --- a/packages/browser/test/transports/offline.test.ts +++ b/packages/browser/test/transports/offline.test.ts @@ -4,8 +4,8 @@ import type { EventItem, InternalBaseTransportOptions, TransportMakeRequestResponse, -} from '@sentry/core'; -import { createEnvelope, createTransport } from '@sentry/core'; +} from '@sentry/core/browser'; +import { createEnvelope, createTransport } from '@sentry/core/browser'; import { TextDecoder, TextEncoder } from 'util'; import { beforeAll, describe, expect, it } from 'vitest'; import { createStore, makeBrowserOfflineTransport, push, shift, unshift } from '../../src/transports/offline'; diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index b80e9ddbfaa5..8efb233764b7 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -4,6 +4,7 @@ "include": ["src/**/*", "test/loader.js"], "compilerOptions": { + "moduleResolution": "bundler", "lib": ["DOM", "es2020", "WebWorker"] } } diff --git a/packages/core/browser.js b/packages/core/browser.js new file mode 100644 index 000000000000..6485bf1fe914 --- /dev/null +++ b/packages/core/browser.js @@ -0,0 +1,3 @@ +// This file is a compatibility shim for bundlers (e.g. webpack 4) that do not +// support the package.json `exports` field for resolving subpath exports. +module.exports = require('./build/cjs/browser.js'); diff --git a/packages/core/package.json b/packages/core/package.json index cf7a9fb42282..6f1bacf40e59 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,13 +10,35 @@ "node": ">=18" }, "files": [ - "/build" + "/build", + "browser.js", + "server.js" ], "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/types/index.d.ts", "exports": { "./package.json": "./package.json", + "./server": { + "import": { + "types": "./build/types/server.d.ts", + "default": "./build/esm/server.js" + }, + "require": { + "types": "./build/types/server.d.ts", + "default": "./build/cjs/server.js" + } + }, + "./browser": { + "import": { + "types": "./build/types/browser.d.ts", + "default": "./build/esm/browser.js" + }, + "require": { + "types": "./build/types/browser.d.ts", + "default": "./build/cjs/browser.js" + } + }, ".": { "import": { "types": "./build/types/index.d.ts", diff --git a/packages/core/rollup.npm.config.mjs b/packages/core/rollup.npm.config.mjs index cc3ad4064820..610de17fadf3 100644 --- a/packages/core/rollup.npm.config.mjs +++ b/packages/core/rollup.npm.config.mjs @@ -14,26 +14,31 @@ if (!packageJson.version) { const packageVersion = packageJson.version; +const settings = { + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to true because we don't want to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? true + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), + }, + plugins: [ + replace({ + preventAssignment: true, + values: { + __SENTRY_SDK_VERSION__: JSON.stringify(packageVersion), + }, + }), + ], + }, +}; + export default makeNPMConfigVariants( makeBaseNPMConfig({ - packageSpecificConfig: { - output: { - // set exports to 'named' or 'auto' so that rollup doesn't warn - exports: 'named', - // set preserveModules to true because we don't want to bundle everything into one file. - preserveModules: - process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined - ? true - : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), - }, - plugins: [ - replace({ - preventAssignment: true, - values: { - __SENTRY_SDK_VERSION__: JSON.stringify(packageVersion), - }, - }), - ], - }, + ...settings, + entrypoints: ['src/index.ts', 'src/server.ts', 'src/browser.ts'], }), ); diff --git a/packages/core/server.js b/packages/core/server.js new file mode 100644 index 000000000000..cc8162c1c03c --- /dev/null +++ b/packages/core/server.js @@ -0,0 +1,3 @@ +// This file is a compatibility shim for bundlers (e.g. webpack 4) that do not +// support the package.json `exports` field for resolving subpath exports. +module.exports = require('./build/cjs/server.js'); diff --git a/packages/core/src/browser-exports.ts b/packages/core/src/browser-exports.ts new file mode 100644 index 000000000000..6750899da1ed --- /dev/null +++ b/packages/core/src/browser-exports.ts @@ -0,0 +1,16 @@ +/** + * Browser-specific utilities for Sentry SDKs + * + * @module + */ +export { getComponentName, getLocationHref, htmlTreeAsString } from './utils/browser'; +export { supportsDOMError, supportsHistory, supportsNativeFetch, supportsReportingObserver } from './utils/supports'; +export type { XhrBreadcrumbData, XhrBreadcrumbHint } from './types-hoist/breadcrumb'; +export type { + HandlerDataXhr, + HandlerDataDom, + HandlerDataHistory, + SentryXhrData, + SentryWrappedXMLHttpRequest, +} from './types-hoist/instrument'; +export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './types-hoist/browseroptions'; diff --git a/packages/core/src/browser.ts b/packages/core/src/browser.ts new file mode 100644 index 000000000000..a0ba2b75ef01 --- /dev/null +++ b/packages/core/src/browser.ts @@ -0,0 +1,4 @@ +/* eslint-disable max-lines */ + +export * from './shared-exports'; +export * from './browser-exports'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1751192d13dc..3e40ce629795 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,603 +1,3 @@ -/* eslint-disable max-lines */ - -export type { ClientClass as SentryCoreCurrentScopes } from './sdk'; -export type { AsyncContextStrategy } from './asyncContext/types'; -export type { Carrier } from './carrier'; -export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; -export type { ServerRuntimeClientOptions } from './server-runtime-client'; -export type { IntegrationIndex } from './integration'; - -export * from './tracing'; -export * from './semanticAttributes'; -export { createEventEnvelope, createSessionEnvelope, createSpanEnvelope } from './envelope'; -export { - captureCheckIn, - withMonitor, - captureException, - captureEvent, - captureMessage, - lastEventId, - close, - flush, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, - setConversationId, - isInitialized, - isEnabled, - startSession, - endSession, - captureSession, - addEventProcessor, -} from './exports'; -export { - getCurrentScope, - getIsolationScope, - getGlobalScope, - withScope, - withIsolationScope, - getClient, - getTraceContextFromScope, - registerExternalPropagationContext, - getExternalPropagationContext, - hasExternalPropagationContext, -} from './currentScopes'; -export { getDefaultCurrentScope, getDefaultIsolationScope } from './defaultScopes'; -export { setAsyncContextStrategy } from './asyncContext'; -export { getGlobalSingleton, getMainCarrier } from './carrier'; -export { makeSession, closeSession, updateSession } from './session'; -export { Scope } from './scope'; -export type { CaptureContext, ScopeContext, ScopeData } from './scope'; -export { notifyEventProcessors } from './eventProcessors'; -export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint, SENTRY_API_VERSION } from './api'; -export { Client } from './client'; -export { ServerRuntimeClient } from './server-runtime-client'; -export { initAndBind, setCurrentClient } from './sdk'; -export { createTransport } from './transports/base'; -export { makeOfflineTransport } from './transports/offline'; -export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from './transports/multiplexed'; -export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; -export { - _INTERNAL_skipAiProviderWrapping, - _INTERNAL_shouldSkipAiProviderWrapping, - _INTERNAL_clearAiProviderSkips, -} from './utils/ai/providerSkip'; -export { envToBool } from './utils/envToBool'; -export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData'; -export { prepareEvent } from './utils/prepareEvent'; -export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; -export { createCheckInEnvelope } from './checkin'; -export { hasSpansEnabled } from './utils/hasSpansEnabled'; -export { withStreamedSpan } from './tracing/spans/beforeSendSpan'; -export { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; -export { safeSetSpanJSONAttributes } from './tracing/spans/captureSpan'; -export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; -export { handleCallbackErrors } from './utils/handleCallbackErrors'; -export { parameterize, fmt } from './utils/parameterize'; -export type { HandleTunnelRequestOptions } from './utils/tunnel'; -export { handleTunnelRequest } from './utils/tunnel'; - -export { addAutoIpAddressToSession } from './utils/ipAddress'; -// eslint-disable-next-line deprecation/deprecation -export { addAutoIpAddressToUser } from './utils/ipAddress'; -export { - convertSpanLinksForEnvelope, - spanToTraceHeader, - spanToJSON, - spanToStreamedSpanJSON, - spanIsSampled, - spanToTraceContext, - getSpanDescendants, - getStatusMessage, - getRootSpan, - INTERNAL_getSegmentSpan, - getActiveSpan, - addChildSpanToSpan, - spanTimeInputToSeconds, - updateSpanName, -} from './utils/spanUtils'; -export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; -export { parseSampleRate } from './utils/parseSampleRate'; -export { applySdkMetadata } from './utils/sdkMetadata'; -export { getTraceData } from './utils/traceData'; -export { shouldPropagateTraceForUrl } from './utils/tracePropagationTargets'; -export { getTraceMetaTags } from './utils/meta'; -export { debounce } from './utils/debounce'; -export { makeWeakRef, derefWeakRef } from './utils/weakRef'; -export type { MaybeWeakRef } from './utils/weakRef'; -export { shouldIgnoreSpan } from './utils/should-ignore-span'; -export { - winterCGHeadersToDict, - winterCGRequestToRequestData, - captureBodyFromWinterCGRequest, - httpRequestToRequestData, - extractQueryParamsFromUrl, - headersToDict, - httpHeadersToSpanAttributes, - getMaxBodyByteLength, - MAX_BODY_BYTE_LENGTH, -} from './utils/request'; -export type { MaxRequestBodySize } from './utils/request'; -export { DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT } from './constants'; -export { addBreadcrumb } from './breadcrumbs'; -export { functionToStringIntegration } from './integrations/functiontostring'; -// eslint-disable-next-line deprecation/deprecation -export { inboundFiltersIntegration } from './integrations/eventFilters'; -export { eventFiltersIntegration } from './integrations/eventFilters'; -export { linkedErrorsIntegration } from './integrations/linkederrors'; -export { moduleMetadataIntegration } from './integrations/moduleMetadata'; -export { requestDataIntegration } from './integrations/requestdata'; -export { captureConsoleIntegration } from './integrations/captureconsole'; -export { patchExpressModule, setupExpressErrorHandler, expressErrorHandler } from './integrations/express/index'; -export type { - ExpressIntegrationOptions, - ExpressHandlerOptions, - ExpressMiddleware, - ExpressErrorMiddleware, -} from './integrations/express/types'; -export { dedupeIntegration } from './integrations/dedupe'; -export { extraErrorDataIntegration } from './integrations/extraerrordata'; -export { rewriteFramesIntegration } from './integrations/rewriteframes'; -export { supabaseIntegration, instrumentSupabaseClient } from './integrations/supabase'; -export { instrumentPostgresJsSql } from './integrations/postgresjs'; -export { zodErrorsIntegration } from './integrations/zoderrors'; -export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; -export { consoleIntegration } from './integrations/console'; -export type { FeatureFlagsIntegration } from './integrations/featureFlags'; -export { featureFlagsIntegration } from './integrations/featureFlags'; -export { growthbookIntegration } from './integrations/featureFlags'; -export { conversationIdIntegration } from './integrations/conversationId'; -export { patchHttpModuleClient } from './integrations/http/client-patch'; -export { getHttpClientSubscriptions } from './integrations/http/client-subscriptions'; -export { addOutgoingRequestBreadcrumb } from './integrations/http/add-outgoing-request-breadcrumb'; -export { - getRequestUrl, - getRequestUrlObject, - getRequestUrlFromClientRequest, - getRequestOptions, -} from './integrations/http/get-request-url'; -export { HTTP_ON_CLIENT_REQUEST, HTTP_ON_SERVER_REQUEST } from './integrations/http/constants'; -export type { - HttpInstrumentationOptions, - HttpClientRequest, - HttpIncomingMessage, - HttpServerResponse, - HttpModuleExport, -} from './integrations/http/types'; - -export { profiler } from './profiling'; -// eslint thinks the entire function is deprecated (while only one overload is actually deprecated) -// Therefore: -// eslint-disable-next-line deprecation/deprecation -export { instrumentFetchRequest, _INTERNAL_getTracingHeadersForFetchRequest } from './fetch'; -export { trpcMiddleware } from './trpc'; -export { wrapMcpServerWithSentry } from './integrations/mcp-server'; -export { captureFeedback } from './feedback'; -export type { ReportDialogOptions } from './report-dialog'; -export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/internal'; -export * as logger from './logs/public-api'; -export { consoleLoggingIntegration } from './logs/console-integration'; -export { - _INTERNAL_captureMetric, - _INTERNAL_flushMetricsBuffer, - _INTERNAL_captureSerializedMetric, -} from './metrics/internal'; -export * as metrics from './metrics/public-api'; -export type { MetricOptions } from './metrics/public-api'; -export { createConsolaReporter } from './integrations/consola'; -export { addVercelAiProcessors } from './tracing/vercel-ai'; -export { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_cleanupToolCallSpanContext } from './tracing/vercel-ai/utils'; -export { toolCallSpanContextMap as _INTERNAL_toolCallSpanContextMap } from './tracing/vercel-ai/constants'; -export { instrumentOpenAiClient } from './tracing/openai'; -export { OPENAI_INTEGRATION_NAME } from './tracing/openai/constants'; -export { instrumentAnthropicAiClient } from './tracing/anthropic-ai'; -export { ANTHROPIC_AI_INTEGRATION_NAME } from './tracing/anthropic-ai/constants'; -export { instrumentGoogleGenAIClient } from './tracing/google-genai'; -export { GOOGLE_GENAI_INTEGRATION_NAME } from './tracing/google-genai/constants'; -export type { GoogleGenAIResponse } from './tracing/google-genai/types'; -export { createLangChainCallbackHandler, instrumentLangChainEmbeddings } from './tracing/langchain'; -export { LANGCHAIN_INTEGRATION_NAME } from './tracing/langchain/constants'; -export type { LangChainOptions, LangChainIntegration } from './tracing/langchain/types'; -export { instrumentStateGraphCompile, instrumentCreateReactAgent, instrumentLangGraph } from './tracing/langgraph'; -export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants'; -export type { LangGraphOptions, LangGraphIntegration, CompiledGraph } from './tracing/langgraph/types'; -export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './tracing/openai/types'; -export type { - AnthropicAiClient, - AnthropicAiOptions, - AnthropicAiInstrumentedMethod, - AnthropicAiResponse, -} from './tracing/anthropic-ai/types'; -export type { - GoogleGenAIClient, - GoogleGenAIChat, - GoogleGenAIOptions, - GoogleGenAIInstrumentedMethod, -} from './tracing/google-genai/types'; -// eslint-disable-next-line deprecation/deprecation -export type { GoogleGenAIIstrumentedMethod } from './tracing/google-genai/types'; - -export { SpanBuffer } from './tracing/spans/spanBuffer'; -export { hasSpanStreamingEnabled } from './tracing/spans/hasSpanStreamingEnabled'; -export { spanStreamingIntegration } from './integrations/spanStreaming'; - -export type { FeatureFlag } from './utils/featureFlags'; - -export { - _INTERNAL_copyFlagsFromScopeToEvent, - _INTERNAL_insertFlagToScope, - _INTERNAL_addFeatureFlagToActiveSpan, - _INTERNAL_FLAG_BUFFER_SIZE, - _INTERNAL_MAX_FLAGS_PER_SPAN, -} from './utils/featureFlags'; - -export { applyAggregateErrorsToEvent } from './utils/aggregate-errors'; -export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils/breadcrumb-log-level'; -export { getComponentName, getLocationHref, htmlTreeAsString } from './utils/browser'; -export { dsnFromString, dsnToString, makeDsn } from './utils/dsn'; -// eslint-disable-next-line deprecation/deprecation -export { SentryError } from './utils/error'; -export { GLOBAL_OBJ } from './utils/worldwide'; -export type { InternalGlobal } from './utils/worldwide'; -export { addConsoleInstrumentationHandler } from './instrument/console'; -export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; -export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; -export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; -export { addHandler, maybeInstrument, resetInstrumentationHandlers, triggerHandlers } from './instrument/handlers'; -export { - isDOMError, - isDOMException, - isElement, - isError, - isErrorEvent, - isEvent, - isInstanceOf, - isParameterizedString, - isPlainObject, - isPrimitive, - isRegExp, - isString, - isSyntheticEvent, - isThenable, - isVueViewModel, -} from './utils/is'; -export { isBrowser } from './utils/isBrowser'; -export { CONSOLE_LEVELS, consoleSandbox, debug, originalConsoleMethods } from './utils/debug-logger'; -export type { SentryDebugLogger } from './utils/debug-logger'; -export { - addContextToFrame, - addExceptionMechanism, - addExceptionTypeValue, - checkOrSetAlreadyCaught, - isAlreadyCaptured, - getEventDescription, - parseSemver, - uuid4, -} from './utils/misc'; -export { isNodeEnv, loadModule } from './utils/node'; -export { normalize, normalizeToSize, normalizeUrlToBase } from './utils/normalize'; -export { setNormalizationDepthOverrideHint, setSkipNormalizationHint } from './utils/normalizationHints'; -export { - addNonEnumerableProperty, - convertToPlainObject, - // eslint-disable-next-line deprecation/deprecation - dropUndefinedKeys, - extractExceptionKeysForMessage, - fill, - getOriginalFunction, - markFunctionWrapped, - objectify, -} from './utils/object'; -export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './utils/path'; -export { makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer'; -export type { PromiseBuffer } from './utils/promisebuffer'; -export { severityLevelFromString } from './utils/severity'; -export { replaceExports } from './utils/exports'; -export { - UNKNOWN_FUNCTION, - createStackParser, - getFramesFromEvent, - getFunctionName, - stackParserFromStackParserOptions, - stripSentryFramesAndReverse, -} from './utils/stacktrace'; -export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace'; -export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils/string'; -export { - isNativeFunction, - supportsDOMError, - supportsDOMException, - supportsErrorEvent, - // eslint-disable-next-line deprecation/deprecation - supportsFetch, - supportsHistory, - supportsNativeFetch, - // eslint-disable-next-line deprecation/deprecation - supportsReferrerPolicy, - supportsReportingObserver, -} from './utils/supports'; -export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise'; -export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils/time'; -export { - TRACEPARENT_REGEXP, - extractTraceparentData, - generateSentryTraceHeader, - propagationContextFromHeaders, - shouldContinueTrace, - generateTraceparentHeader, -} from './utils/tracing'; -export { getSDKSource, isBrowserBundle } from './utils/env'; -export type { SdkSource } from './utils/env'; -export { - addItemToEnvelope, - createAttachmentEnvelopeItem, - createEnvelope, - createEventEnvelopeHeaders, - createSpanEnvelopeItem, - envelopeContainsItemType, - envelopeItemTypeToDataCategory, - forEachEnvelopeItem, - getSdkMetadataForEnvelopeHeader, - parseEnvelope, - serializeEnvelope, -} from './utils/envelope'; -export { createClientReportEnvelope } from './utils/clientreport'; -export { - DEFAULT_RETRY_AFTER, - disabledUntil, - isRateLimited, - parseRetryAfterHeader, - updateRateLimits, -} from './utils/ratelimit'; -export type { RateLimits } from './utils/ratelimit'; -export { - MAX_BAGGAGE_STRING_LENGTH, - SENTRY_BAGGAGE_KEY_PREFIX, - SENTRY_BAGGAGE_KEY_PREFIX_REGEX, - baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, - parseBaggageHeader, - objectToBaggageHeader, - mergeBaggageHeaders, -} from './utils/baggage'; -export { - getSanitizedUrlString, - parseUrl, - stripUrlQueryAndFragment, - parseStringToURLObject, - getHttpSpanDetailsFromUrlObject, - isURLObjectRelative, - getSanitizedUrlStringFromUrlObject, - stripDataUrlContent, -} from './utils/url'; -export { - eventFromMessage, - eventFromUnknownInput, - exceptionFromError, - parseStackFrames, - _enhanceErrorWithSentryInfo as _INTERNAL_enhanceErrorWithSentryInfo, -} from './utils/eventbuilder'; -export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; -export { LRUMap } from './utils/lru'; -export { generateTraceId, generateSpanId } from './utils/propagationContext'; -export { vercelWaitUntil } from './utils/vercelWaitUntil'; -export { flushIfServerless } from './utils/flushIfServerless'; -export { SDK_VERSION } from './utils/version'; -export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids'; -export { getFilenameToMetadataMap } from './metadata'; -export { escapeStringForRegex } from './vendor/escapeStringForRegex'; - -export type { Attachment } from './types-hoist/attachment'; -export type { - Breadcrumb, - BreadcrumbHint, - FetchBreadcrumbData, - XhrBreadcrumbData, - FetchBreadcrumbHint, - XhrBreadcrumbHint, -} from './types-hoist/breadcrumb'; -export type { ClientReport, Outcome, EventDropReason } from './types-hoist/clientreport'; -export type { - Context, - Contexts, - DeviceContext, - OsContext, - AppContext, - CultureContext, - TraceContext, - CloudResourceContext, - MissingInstrumentationContext, -} from './types-hoist/context'; -export type { DataCategory } from './types-hoist/datacategory'; -export type { DsnComponents, DsnLike, DsnProtocol } from './types-hoist/dsn'; -export type { DebugImage, DebugMeta } from './types-hoist/debugMeta'; -export type { - AttachmentItem, - BaseEnvelopeHeaders, - BaseEnvelopeItemHeaders, - ClientReportEnvelope, - ClientReportItem, - DynamicSamplingContext, - Envelope, - EnvelopeItemType, - EnvelopeItem, - EventEnvelope, - EventEnvelopeHeaders, - EventItem, - ReplayEnvelope, - FeedbackItem, - SessionEnvelope, - SessionItem, - UserFeedbackItem, - CheckInItem, - CheckInEnvelope, - RawSecurityEnvelope, - RawSecurityItem, - ProfileItem, - ProfileChunkEnvelope, - ProfileChunkItem, - SpanEnvelope, - StreamedSpanEnvelope, - SpanItem, - LogEnvelope, - MetricEnvelope, -} from './types-hoist/envelope'; -export type { ExtendedError } from './types-hoist/error'; -export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './types-hoist/event'; -export type { EventProcessor } from './types-hoist/eventprocessor'; -export type { Exception } from './types-hoist/exception'; -export type { Extra, Extras } from './types-hoist/extra'; -export type { Integration, IntegrationFn } from './types-hoist/integration'; -export type { Mechanism } from './types-hoist/mechanism'; -export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './types-hoist/misc'; -export type { ClientOptions, CoreOptions as Options, ServerRuntimeOptions } from './types-hoist/options'; -export type { Package } from './types-hoist/package'; -export type { PolymorphicEvent, PolymorphicRequest } from './types-hoist/polymorphics'; -export type { - ThreadId, - FrameId, - StackId, - ThreadCpuSample, - ThreadCpuStack, - ThreadCpuFrame, - ThreadCpuProfile, - ContinuousThreadCpuProfile, - Profile, - ProfileChunk, -} from './types-hoist/profiling'; -export type { - ReplayEndEvent, - ReplayEvent, - ReplayRecordingData, - ReplayRecordingMode, - ReplayStartEvent, - ReplayStopReason, -} from './types-hoist/replay'; -export type { - FeedbackErrorCode, - FeedbackErrorMessages, - FeedbackEvent, - FeedbackFormData, - FeedbackInternalOptions, - FeedbackModalIntegration, - FeedbackScreenshotIntegration, - SendFeedback, - SendFeedbackParams, - UserFeedback, -} from './types-hoist/feedback'; -export type { - QueryParams, - RequestEventData, - RequestHookInfo, - ResponseHookInfo, - SanitizedRequestData, -} from './types-hoist/request'; -export type { Runtime } from './types-hoist/runtime'; -export type { SdkInfo } from './types-hoist/sdkinfo'; -export type { SdkMetadata } from './types-hoist/sdkmetadata'; -export type { - SessionAggregates, - AggregationCounts, - Session, - SessionContext, - SessionStatus, - SerializedSession, -} from './types-hoist/session'; -export type { SeverityLevel } from './types-hoist/severity'; -export type { - Span, - SentrySpanArguments, - SpanOrigin, - SpanAttributeValue, - SpanAttributes, - SpanTimeInput, - SpanJSON, - SpanContextData, - TraceFlag, - SerializedStreamedSpan, - SerializedStreamedSpanContainer, - StreamedSpanJSON, -} from './types-hoist/span'; -export type { SpanStatus } from './types-hoist/spanStatus'; -export type { Log, LogSeverityLevel } from './types-hoist/log'; -export type { SpanLink } from './types-hoist/link'; -export type { - Metric, - MetricType, - SerializedMetric, - SerializedMetricContainer, - // eslint-disable-next-line deprecation/deprecation - SerializedMetricAttributeValue, -} from './types-hoist/metric'; -export type { TimedEvent } from './types-hoist/timedEvent'; -export type { StackFrame } from './types-hoist/stackframe'; -export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './types-hoist/stacktrace'; -export type { PropagationContext, TracePropagationTargets, SerializedTraceData } from './types-hoist/tracing'; -export type { StartSpanOptions } from './types-hoist/startSpanOptions'; -export type { TraceparentData, TransactionSource } from './types-hoist/transaction'; -export type { - TracesSamplerSamplingContext, - CustomSamplingContext, - SamplingContext, -} from './types-hoist/samplingcontext'; -export type { - DurationUnit, - InformationUnit, - FractionUnit, - MeasurementUnit, - NoneUnit, - Measurements, -} from './types-hoist/measurement'; -export type { Thread } from './types-hoist/thread'; -export type { - Transport, - TransportRequest, - TransportMakeRequestResponse, - InternalBaseTransportOptions, - BaseTransportOptions, - TransportRequestExecutor, -} from './types-hoist/transport'; -export type { User } from './types-hoist/user'; -export type { WebFetchHeaders, WebFetchRequest } from './types-hoist/webfetchapi'; -export type { WrappedFunction } from './types-hoist/wrappedfunction'; -export type { - HandlerDataFetch, - HandlerDataXhr, - HandlerDataDom, - HandlerDataConsole, - HandlerDataHistory, - HandlerDataError, - HandlerDataUnhandledRejection, - ConsoleLevel, - SentryXhrData, - SentryWrappedXMLHttpRequest, -} from './types-hoist/instrument'; -export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './types-hoist/browseroptions'; -export type { - CheckIn, - MonitorConfig, - FinishedCheckIn, - InProgressCheckIn, - SerializedCheckIn, -} from './types-hoist/checkin'; -export type { ParameterizedString } from './types-hoist/parameterize'; -export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './types-hoist/profiling'; -export type { ViewHierarchyData, ViewHierarchyWindow } from './types-hoist/view-hierarchy'; -export type { LegacyCSPReport } from './types-hoist/csp'; -export type { SerializedLog, SerializedLogContainer } from './types-hoist/log'; -export type { - BuildTimeOptionsBase, - UnstableVitePluginOptions, - UnstableRollupPluginOptions, - UnstableWebpackPluginOptions, -} from './build-time-plugins/buildTimeOptionsBase'; -export type { RandomSafeContextRunner as _INTERNAL_RandomSafeContextRunner } from './utils/randomSafeContext'; -export { - withRandomSafeContext as _INTERNAL_withRandomSafeContext, - safeMathRandom as _INTERNAL_safeMathRandom, - safeDateNow as _INTERNAL_safeDateNow, -} from './utils/randomSafeContext'; -export { safeUnref as _INTERNAL_safeUnref } from './utils/timer'; +export * from './shared-exports'; +export * from './server-exports'; +export * from './browser-exports'; diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts new file mode 100644 index 000000000000..f5ad45486cb5 --- /dev/null +++ b/packages/core/src/server-exports.ts @@ -0,0 +1,43 @@ +/** + * Server-only utilities for Sentry SDKs. + * + * @module + */ + +export type { ServerRuntimeClientOptions } from './server-runtime-client'; +export { ServerRuntimeClient } from './server-runtime-client'; +export type { ServerRuntimeOptions } from './types-hoist/options'; +export { trpcMiddleware } from './trpc'; +export { wrapMcpServerWithSentry } from './integrations/mcp-server'; +export { isNodeEnv, loadModule } from './utils/node'; +export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace'; +export { vercelWaitUntil } from './utils/vercelWaitUntil'; +export { flushIfServerless } from './utils/flushIfServerless'; +export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; +export { safeUnref as _INTERNAL_safeUnref } from './utils/timer'; +export { patchExpressModule, setupExpressErrorHandler, expressErrorHandler } from './integrations/express/index'; +export type { + ExpressIntegrationOptions, + ExpressHandlerOptions, + ExpressMiddleware, + ExpressErrorMiddleware, +} from './integrations/express/types'; +export { instrumentPostgresJsSql } from './integrations/postgresjs'; + +export { patchHttpModuleClient } from './integrations/http/client-patch'; +export { getHttpClientSubscriptions } from './integrations/http/client-subscriptions'; +export { addOutgoingRequestBreadcrumb } from './integrations/http/add-outgoing-request-breadcrumb'; +export { + getRequestUrl, + getRequestUrlObject, + getRequestUrlFromClientRequest, + getRequestOptions, +} from './integrations/http/get-request-url'; +export { HTTP_ON_CLIENT_REQUEST, HTTP_ON_SERVER_REQUEST } from './integrations/http/constants'; +export type { + HttpInstrumentationOptions, + HttpClientRequest, + HttpIncomingMessage, + HttpServerResponse, + HttpModuleExport, +} from './integrations/http/types'; diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts new file mode 100644 index 000000000000..10c4a02625bf --- /dev/null +++ b/packages/core/src/server.ts @@ -0,0 +1,11 @@ +/** + * The Sentry core SDK and integrations used by node, node-core, cloudflare, + * bun, deno, aws lambda, and other server-side platforms, where bundle size + * is less of an issue. + * + * This export should not contain anything strictly browser-specific. + */ +/* eslint-disable max-lines */ + +export * from './shared-exports'; +export * from './server-exports'; diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts new file mode 100644 index 000000000000..ad84415a750f --- /dev/null +++ b/packages/core/src/shared-exports.ts @@ -0,0 +1,545 @@ +/** + * Utilites shared between server and browser SDKs. + */ +/* eslint-disable max-lines */ + +export type { ClientClass as SentryCoreCurrentScopes } from './sdk'; +export type { AsyncContextStrategy } from './asyncContext/types'; +export type { Carrier } from './carrier'; +export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; +export type { IntegrationIndex } from './integration'; +export * from './tracing'; +export * from './semanticAttributes'; +export { createEventEnvelope, createSessionEnvelope, createSpanEnvelope } from './envelope'; +export { + captureCheckIn, + withMonitor, + captureException, + captureEvent, + captureMessage, + lastEventId, + close, + flush, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + setConversationId, + isInitialized, + isEnabled, + startSession, + endSession, + captureSession, + addEventProcessor, +} from './exports'; +export { + getCurrentScope, + getIsolationScope, + getGlobalScope, + withScope, + withIsolationScope, + getClient, + getTraceContextFromScope, + registerExternalPropagationContext, + getExternalPropagationContext, + hasExternalPropagationContext, +} from './currentScopes'; +export { getDefaultCurrentScope, getDefaultIsolationScope } from './defaultScopes'; +export { setAsyncContextStrategy } from './asyncContext'; +export { getGlobalSingleton, getMainCarrier } from './carrier'; +export { makeSession, closeSession, updateSession } from './session'; +export { Scope } from './scope'; +export type { CaptureContext, ScopeContext, ScopeData } from './scope'; +export { notifyEventProcessors } from './eventProcessors'; +export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint, SENTRY_API_VERSION } from './api'; +export { Client } from './client'; +export { initAndBind, setCurrentClient } from './sdk'; +export { createTransport } from './transports/base'; +export { makeOfflineTransport } from './transports/offline'; +export { makeMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from './transports/multiplexed'; +export { getIntegrationsToSetup, addIntegration, defineIntegration, installedIntegrations } from './integration'; +export { + _INTERNAL_skipAiProviderWrapping, + _INTERNAL_shouldSkipAiProviderWrapping, + _INTERNAL_clearAiProviderSkips, +} from './utils/ai/providerSkip'; +export { envToBool } from './utils/envToBool'; +export { applyScopeDataToEvent, mergeScopeData, getCombinedScopeData } from './utils/scopeData'; +export { prepareEvent } from './utils/prepareEvent'; +export type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; +export { createCheckInEnvelope } from './checkin'; +export { hasSpansEnabled } from './utils/hasSpansEnabled'; +export { withStreamedSpan } from './tracing/spans/beforeSendSpan'; +export { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; +export { safeSetSpanJSONAttributes } from './tracing/spans/captureSpan'; +export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; +export { handleCallbackErrors } from './utils/handleCallbackErrors'; +export { parameterize, fmt } from './utils/parameterize'; +export type { HandleTunnelRequestOptions } from './utils/tunnel'; +export { handleTunnelRequest } from './utils/tunnel'; +export { addAutoIpAddressToSession } from './utils/ipAddress'; +// eslint-disable-next-line deprecation/deprecation +export { addAutoIpAddressToUser } from './utils/ipAddress'; +export { + convertSpanLinksForEnvelope, + spanToTraceHeader, + spanToJSON, + spanToStreamedSpanJSON, + spanIsSampled, + spanToTraceContext, + getSpanDescendants, + getStatusMessage, + getRootSpan, + INTERNAL_getSegmentSpan, + getActiveSpan, + addChildSpanToSpan, + spanTimeInputToSeconds, + updateSpanName, +} from './utils/spanUtils'; +export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; +export { parseSampleRate } from './utils/parseSampleRate'; +export { applySdkMetadata } from './utils/sdkMetadata'; +export { getTraceData } from './utils/traceData'; +export { shouldPropagateTraceForUrl } from './utils/tracePropagationTargets'; +export { getTraceMetaTags } from './utils/meta'; +export { debounce } from './utils/debounce'; +export { makeWeakRef, derefWeakRef } from './utils/weakRef'; +export type { MaybeWeakRef } from './utils/weakRef'; +export { shouldIgnoreSpan } from './utils/should-ignore-span'; +export { + winterCGHeadersToDict, + winterCGRequestToRequestData, + captureBodyFromWinterCGRequest, + httpRequestToRequestData, + extractQueryParamsFromUrl, + headersToDict, + httpHeadersToSpanAttributes, + getMaxBodyByteLength, + MAX_BODY_BYTE_LENGTH, +} from './utils/request'; +export type { MaxRequestBodySize } from './utils/request'; +export { DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT } from './constants'; +export { addBreadcrumb } from './breadcrumbs'; +export { functionToStringIntegration } from './integrations/functiontostring'; +// eslint-disable-next-line deprecation/deprecation +export { inboundFiltersIntegration } from './integrations/eventFilters'; +export { eventFiltersIntegration } from './integrations/eventFilters'; +export { linkedErrorsIntegration } from './integrations/linkederrors'; +export { moduleMetadataIntegration } from './integrations/moduleMetadata'; +export { requestDataIntegration } from './integrations/requestdata'; +export { captureConsoleIntegration } from './integrations/captureconsole'; +export { dedupeIntegration } from './integrations/dedupe'; +export { extraErrorDataIntegration } from './integrations/extraerrordata'; +export { rewriteFramesIntegration } from './integrations/rewriteframes'; +export { supabaseIntegration, instrumentSupabaseClient } from './integrations/supabase'; +export { zodErrorsIntegration } from './integrations/zoderrors'; +export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; +export { consoleIntegration } from './integrations/console'; +export type { FeatureFlagsIntegration } from './integrations/featureFlags'; +export { featureFlagsIntegration } from './integrations/featureFlags'; +export { growthbookIntegration } from './integrations/featureFlags'; +export { conversationIdIntegration } from './integrations/conversationId'; +export { profiler } from './profiling'; +// eslint thinks the entire function is deprecated (while only one overload is actually deprecated) +// Therefore: +// eslint-disable-next-line deprecation/deprecation +export { instrumentFetchRequest, _INTERNAL_getTracingHeadersForFetchRequest } from './fetch'; +export { captureFeedback } from './feedback'; +export type { ReportDialogOptions } from './report-dialog'; +export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/internal'; +export * as logger from './logs/public-api'; +export { consoleLoggingIntegration } from './logs/console-integration'; +export { + _INTERNAL_captureMetric, + _INTERNAL_flushMetricsBuffer, + _INTERNAL_captureSerializedMetric, +} from './metrics/internal'; +export * as metrics from './metrics/public-api'; +export type { MetricOptions } from './metrics/public-api'; +export { createConsolaReporter } from './integrations/consola'; +export { addVercelAiProcessors } from './tracing/vercel-ai'; +export { _INTERNAL_getSpanContextForToolCallId, _INTERNAL_cleanupToolCallSpanContext } from './tracing/vercel-ai/utils'; +export { toolCallSpanContextMap as _INTERNAL_toolCallSpanContextMap } from './tracing/vercel-ai/constants'; +export { instrumentOpenAiClient } from './tracing/openai'; +export { OPENAI_INTEGRATION_NAME } from './tracing/openai/constants'; +export { instrumentAnthropicAiClient } from './tracing/anthropic-ai'; +export { ANTHROPIC_AI_INTEGRATION_NAME } from './tracing/anthropic-ai/constants'; +export { instrumentGoogleGenAIClient } from './tracing/google-genai'; +export { GOOGLE_GENAI_INTEGRATION_NAME } from './tracing/google-genai/constants'; +export type { GoogleGenAIResponse } from './tracing/google-genai/types'; +export { createLangChainCallbackHandler, instrumentLangChainEmbeddings } from './tracing/langchain'; +export { LANGCHAIN_INTEGRATION_NAME } from './tracing/langchain/constants'; +export type { LangChainOptions, LangChainIntegration } from './tracing/langchain/types'; +export { instrumentStateGraphCompile, instrumentCreateReactAgent, instrumentLangGraph } from './tracing/langgraph'; +export { LANGGRAPH_INTEGRATION_NAME } from './tracing/langgraph/constants'; +export type { LangGraphOptions, LangGraphIntegration, CompiledGraph } from './tracing/langgraph/types'; +export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './tracing/openai/types'; +export type { + AnthropicAiClient, + AnthropicAiOptions, + AnthropicAiInstrumentedMethod, + AnthropicAiResponse, +} from './tracing/anthropic-ai/types'; +export type { + GoogleGenAIClient, + GoogleGenAIChat, + GoogleGenAIOptions, + GoogleGenAIInstrumentedMethod, +} from './tracing/google-genai/types'; +// eslint-disable-next-line deprecation/deprecation +export type { GoogleGenAIIstrumentedMethod } from './tracing/google-genai/types'; +export { SpanBuffer } from './tracing/spans/spanBuffer'; +export { hasSpanStreamingEnabled } from './tracing/spans/hasSpanStreamingEnabled'; +export { spanStreamingIntegration } from './integrations/spanStreaming'; +export type { FeatureFlag } from './utils/featureFlags'; +export { + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_FLAG_BUFFER_SIZE, + _INTERNAL_MAX_FLAGS_PER_SPAN, +} from './utils/featureFlags'; +export { applyAggregateErrorsToEvent } from './utils/aggregate-errors'; +export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils/breadcrumb-log-level'; +export { dsnFromString, dsnToString, makeDsn } from './utils/dsn'; +// eslint-disable-next-line deprecation/deprecation +export { SentryError } from './utils/error'; +export { GLOBAL_OBJ } from './utils/worldwide'; +export type { InternalGlobal } from './utils/worldwide'; +export { addConsoleInstrumentationHandler } from './instrument/console'; +export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; +export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; +export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; +export { addHandler, maybeInstrument, resetInstrumentationHandlers, triggerHandlers } from './instrument/handlers'; +export { + isDOMError, + isDOMException, + isElement, + isError, + isErrorEvent, + isEvent, + isInstanceOf, + isParameterizedString, + isPlainObject, + isPrimitive, + isRegExp, + isString, + isSyntheticEvent, + isThenable, + isVueViewModel, +} from './utils/is'; +export { isBrowser } from './utils/isBrowser'; +export { CONSOLE_LEVELS, consoleSandbox, debug, originalConsoleMethods } from './utils/debug-logger'; +export type { SentryDebugLogger } from './utils/debug-logger'; +export { + addContextToFrame, + addExceptionMechanism, + addExceptionTypeValue, + checkOrSetAlreadyCaught, + isAlreadyCaptured, + getEventDescription, + parseSemver, + uuid4, +} from './utils/misc'; +export { normalize, normalizeToSize, normalizeUrlToBase } from './utils/normalize'; +export { setNormalizationDepthOverrideHint, setSkipNormalizationHint } from './utils/normalizationHints'; +export { + addNonEnumerableProperty, + convertToPlainObject, + // eslint-disable-next-line deprecation/deprecation + dropUndefinedKeys, + extractExceptionKeysForMessage, + fill, + getOriginalFunction, + markFunctionWrapped, + objectify, +} from './utils/object'; +export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './utils/path'; +export { makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer'; +export type { PromiseBuffer } from './utils/promisebuffer'; +export { severityLevelFromString } from './utils/severity'; +export { replaceExports } from './utils/exports'; +export { + UNKNOWN_FUNCTION, + createStackParser, + getFramesFromEvent, + getFunctionName, + stackParserFromStackParserOptions, + stripSentryFramesAndReverse, +} from './utils/stacktrace'; +export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils/string'; +export { + isNativeFunction, + supportsDOMException, + supportsErrorEvent, + // eslint-disable-next-line deprecation/deprecation + supportsFetch, + // eslint-disable-next-line deprecation/deprecation + supportsReferrerPolicy, +} from './utils/supports'; +export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise'; +export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils/time'; +export { + TRACEPARENT_REGEXP, + extractTraceparentData, + generateSentryTraceHeader, + propagationContextFromHeaders, + shouldContinueTrace, + generateTraceparentHeader, +} from './utils/tracing'; +export { getSDKSource, isBrowserBundle } from './utils/env'; +export type { SdkSource } from './utils/env'; +export { + addItemToEnvelope, + createAttachmentEnvelopeItem, + createEnvelope, + createEventEnvelopeHeaders, + createSpanEnvelopeItem, + envelopeContainsItemType, + envelopeItemTypeToDataCategory, + forEachEnvelopeItem, + getSdkMetadataForEnvelopeHeader, + parseEnvelope, + serializeEnvelope, +} from './utils/envelope'; +export { createClientReportEnvelope } from './utils/clientreport'; +export { + DEFAULT_RETRY_AFTER, + disabledUntil, + isRateLimited, + parseRetryAfterHeader, + updateRateLimits, +} from './utils/ratelimit'; +export type { RateLimits } from './utils/ratelimit'; +export { + MAX_BAGGAGE_STRING_LENGTH, + SENTRY_BAGGAGE_KEY_PREFIX, + SENTRY_BAGGAGE_KEY_PREFIX_REGEX, + baggageHeaderToDynamicSamplingContext, + dynamicSamplingContextToSentryBaggageHeader, + parseBaggageHeader, + objectToBaggageHeader, + mergeBaggageHeaders, +} from './utils/baggage'; +export { + getSanitizedUrlString, + parseUrl, + stripUrlQueryAndFragment, + parseStringToURLObject, + getHttpSpanDetailsFromUrlObject, + isURLObjectRelative, + getSanitizedUrlStringFromUrlObject, + stripDataUrlContent, +} from './utils/url'; +export { + eventFromMessage, + eventFromUnknownInput, + exceptionFromError, + parseStackFrames, + _enhanceErrorWithSentryInfo as _INTERNAL_enhanceErrorWithSentryInfo, +} from './utils/eventbuilder'; +export { LRUMap } from './utils/lru'; +export { generateTraceId, generateSpanId } from './utils/propagationContext'; +export { SDK_VERSION } from './utils/version'; +export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids'; +export { getFilenameToMetadataMap } from './metadata'; +export { escapeStringForRegex } from './vendor/escapeStringForRegex'; +export type { Attachment } from './types-hoist/attachment'; +export type { Breadcrumb, BreadcrumbHint, FetchBreadcrumbData, FetchBreadcrumbHint } from './types-hoist/breadcrumb'; +export type { ClientReport, Outcome, EventDropReason } from './types-hoist/clientreport'; +export type { + Context, + Contexts, + DeviceContext, + OsContext, + AppContext, + CultureContext, + TraceContext, + CloudResourceContext, + MissingInstrumentationContext, +} from './types-hoist/context'; +export type { DataCategory } from './types-hoist/datacategory'; +export type { DsnComponents, DsnLike, DsnProtocol } from './types-hoist/dsn'; +export type { DebugImage, DebugMeta } from './types-hoist/debugMeta'; +export type { + AttachmentItem, + BaseEnvelopeHeaders, + BaseEnvelopeItemHeaders, + ClientReportEnvelope, + ClientReportItem, + DynamicSamplingContext, + Envelope, + EnvelopeItemType, + EnvelopeItem, + EventEnvelope, + EventEnvelopeHeaders, + EventItem, + ReplayEnvelope, + FeedbackItem, + SessionEnvelope, + SessionItem, + UserFeedbackItem, + CheckInItem, + CheckInEnvelope, + RawSecurityEnvelope, + RawSecurityItem, + ProfileItem, + ProfileChunkEnvelope, + ProfileChunkItem, + SpanEnvelope, + StreamedSpanEnvelope, + SpanItem, + LogEnvelope, + MetricEnvelope, +} from './types-hoist/envelope'; +export type { ExtendedError } from './types-hoist/error'; +export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './types-hoist/event'; +export type { EventProcessor } from './types-hoist/eventprocessor'; +export type { Exception } from './types-hoist/exception'; +export type { Extra, Extras } from './types-hoist/extra'; +export type { Integration, IntegrationFn } from './types-hoist/integration'; +export type { Mechanism } from './types-hoist/mechanism'; +export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './types-hoist/misc'; +export type { ClientOptions, CoreOptions as Options } from './types-hoist/options'; +export type { Package } from './types-hoist/package'; +export type { PolymorphicEvent, PolymorphicRequest } from './types-hoist/polymorphics'; +export type { + ThreadId, + FrameId, + StackId, + ThreadCpuSample, + ThreadCpuStack, + ThreadCpuFrame, + ThreadCpuProfile, + ContinuousThreadCpuProfile, + Profile, + ProfileChunk, +} from './types-hoist/profiling'; +export type { + ReplayEndEvent, + ReplayEvent, + ReplayRecordingData, + ReplayRecordingMode, + ReplayStartEvent, + ReplayStopReason, +} from './types-hoist/replay'; +export type { + FeedbackErrorCode, + FeedbackErrorMessages, + FeedbackEvent, + FeedbackFormData, + FeedbackInternalOptions, + FeedbackModalIntegration, + FeedbackScreenshotIntegration, + SendFeedback, + SendFeedbackParams, + UserFeedback, +} from './types-hoist/feedback'; +export type { + QueryParams, + RequestEventData, + RequestHookInfo, + ResponseHookInfo, + SanitizedRequestData, +} from './types-hoist/request'; +export type { Runtime } from './types-hoist/runtime'; +export type { SdkInfo } from './types-hoist/sdkinfo'; +export type { SdkMetadata } from './types-hoist/sdkmetadata'; +export type { + SessionAggregates, + AggregationCounts, + Session, + SessionContext, + SessionStatus, + SerializedSession, +} from './types-hoist/session'; +export type { SeverityLevel } from './types-hoist/severity'; +export type { + Span, + SentrySpanArguments, + SpanOrigin, + SpanAttributeValue, + SpanAttributes, + SpanTimeInput, + SpanJSON, + SpanContextData, + TraceFlag, + SerializedStreamedSpan, + SerializedStreamedSpanContainer, + StreamedSpanJSON, +} from './types-hoist/span'; +export type { SpanStatus } from './types-hoist/spanStatus'; +export type { Log, LogSeverityLevel } from './types-hoist/log'; +export type { SpanLink } from './types-hoist/link'; +export type { + Metric, + MetricType, + SerializedMetric, + SerializedMetricContainer, + // eslint-disable-next-line deprecation/deprecation + SerializedMetricAttributeValue, +} from './types-hoist/metric'; +export type { TimedEvent } from './types-hoist/timedEvent'; +export type { StackFrame } from './types-hoist/stackframe'; +export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './types-hoist/stacktrace'; +export type { PropagationContext, TracePropagationTargets, SerializedTraceData } from './types-hoist/tracing'; +export type { StartSpanOptions } from './types-hoist/startSpanOptions'; +export type { TraceparentData, TransactionSource } from './types-hoist/transaction'; +export type { + TracesSamplerSamplingContext, + CustomSamplingContext, + SamplingContext, +} from './types-hoist/samplingcontext'; +export type { + DurationUnit, + InformationUnit, + FractionUnit, + MeasurementUnit, + NoneUnit, + Measurements, +} from './types-hoist/measurement'; +export type { Thread } from './types-hoist/thread'; +export type { + Transport, + TransportRequest, + TransportMakeRequestResponse, + InternalBaseTransportOptions, + BaseTransportOptions, + TransportRequestExecutor, +} from './types-hoist/transport'; +export type { User } from './types-hoist/user'; +export type { WebFetchHeaders, WebFetchRequest } from './types-hoist/webfetchapi'; +export type { WrappedFunction } from './types-hoist/wrappedfunction'; +export type { + HandlerDataFetch, + HandlerDataConsole, + HandlerDataError, + HandlerDataUnhandledRejection, + ConsoleLevel, +} from './types-hoist/instrument'; +export type { + CheckIn, + MonitorConfig, + FinishedCheckIn, + InProgressCheckIn, + SerializedCheckIn, +} from './types-hoist/checkin'; +export type { ParameterizedString } from './types-hoist/parameterize'; +export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './types-hoist/profiling'; +export type { ViewHierarchyData, ViewHierarchyWindow } from './types-hoist/view-hierarchy'; +export type { LegacyCSPReport } from './types-hoist/csp'; +export type { SerializedLog, SerializedLogContainer } from './types-hoist/log'; +export type { + BuildTimeOptionsBase, + UnstableVitePluginOptions, + UnstableRollupPluginOptions, + UnstableWebpackPluginOptions, +} from './build-time-plugins/buildTimeOptionsBase'; +export type { RandomSafeContextRunner as _INTERNAL_RandomSafeContextRunner } from './utils/randomSafeContext'; +export { + withRandomSafeContext as _INTERNAL_withRandomSafeContext, + safeMathRandom as _INTERNAL_safeMathRandom, + safeDateNow as _INTERNAL_safeDateNow, +} from './utils/randomSafeContext'; diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json index 5a80d11f7055..40725a892888 100644 --- a/packages/core/tsconfig.test.json +++ b/packages/core/tsconfig.test.json @@ -5,7 +5,6 @@ "compilerOptions": { "lib": ["DOM", "es2020"], - "module": "ESNext", // support dynamic import() // should include all types from `./tsconfig.json` plus types for all test frameworks used "types": ["node"] diff --git a/packages/core/tsconfig.types.json b/packages/core/tsconfig.types.json index 65455f66bd75..de9b6935694a 100644 --- a/packages/core/tsconfig.types.json +++ b/packages/core/tsconfig.types.json @@ -2,6 +2,8 @@ "extends": "./tsconfig.json", "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, diff --git a/packages/effect/rollup.npm.config.mjs b/packages/effect/rollup.npm.config.mjs index 211157646473..dde3327a2501 100644 --- a/packages/effect/rollup.npm.config.mjs +++ b/packages/effect/rollup.npm.config.mjs @@ -9,9 +9,11 @@ const baseConfig = makeBaseNPMConfig({ }, }); -const defaultExternal = baseConfig.external || []; +const defaultExternal = baseConfig.external; +const isDefaultExternal = + typeof defaultExternal === 'function' ? defaultExternal : id => (defaultExternal || []).includes(id); baseConfig.external = id => { - if (defaultExternal.includes(id)) { + if (isDefaultExternal(id)) { return true; } diff --git a/packages/ember/tsconfig.json b/packages/ember/tsconfig.json index e472924f4d0f..04eae83278b0 100644 --- a/packages/ember/tsconfig.json +++ b/packages/ember/tsconfig.json @@ -4,7 +4,7 @@ "target": "es2022", "lib": ["DOM", "ES2022"], "allowJs": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "alwaysStrict": true, "strictNullChecks": true, diff --git a/packages/hono/rollup.npm.config.mjs b/packages/hono/rollup.npm.config.mjs index 2a03d7540bdc..83a43b881f1f 100644 --- a/packages/hono/rollup.npm.config.mjs +++ b/packages/hono/rollup.npm.config.mjs @@ -11,7 +11,7 @@ const baseConfig = makeBaseNPMConfig({ const defaultExternal = baseConfig.external; baseConfig.external = id => { - if (defaultExternal.includes(id)) { + if (defaultExternal(id)) { return true; } // Mark all hono subpaths as external diff --git a/packages/integration-shims/src/BrowserTracing.ts b/packages/integration-shims/src/BrowserTracing.ts index f92f3a63d6d8..aaafbd537ec8 100644 --- a/packages/integration-shims/src/BrowserTracing.ts +++ b/packages/integration-shims/src/BrowserTracing.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, defineIntegration } from '@sentry/core'; +import { consoleSandbox, defineIntegration } from '@sentry/core/browser'; /** * This is a shim for the BrowserTracing integration. diff --git a/packages/integration-shims/src/ElementTiming.ts b/packages/integration-shims/src/ElementTiming.ts index 8a521163f7e1..1eb43d9cf682 100644 --- a/packages/integration-shims/src/ElementTiming.ts +++ b/packages/integration-shims/src/ElementTiming.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, defineIntegration } from '@sentry/core'; +import { consoleSandbox, defineIntegration } from '@sentry/core/browser'; /** * This is a shim for the ElementTiming integration. diff --git a/packages/integration-shims/src/Feedback.ts b/packages/integration-shims/src/Feedback.ts index dd56bd273717..aa21f81494e7 100644 --- a/packages/integration-shims/src/Feedback.ts +++ b/packages/integration-shims/src/Feedback.ts @@ -1,5 +1,5 @@ -import type { Integration } from '@sentry/core'; -import { consoleSandbox } from '@sentry/core'; +import type { Integration } from '@sentry/core/browser'; +import { consoleSandbox } from '@sentry/core/browser'; import { FAKE_FUNCTION } from './common'; const FEEDBACK_INTEGRATION_METHODS = ['attachTo', 'createForm', 'createWidget', 'remove'] as const; diff --git a/packages/integration-shims/src/Replay.ts b/packages/integration-shims/src/Replay.ts index eee5cfbb2ef7..e532c49bf276 100644 --- a/packages/integration-shims/src/Replay.ts +++ b/packages/integration-shims/src/Replay.ts @@ -1,5 +1,5 @@ -import type { Integration } from '@sentry/core'; -import { consoleSandbox } from '@sentry/core'; +import type { Integration } from '@sentry/core/browser'; +import { consoleSandbox } from '@sentry/core/browser'; import { FAKE_FUNCTION } from './common'; const REPLAY_INTEGRATION_METHODS = ['start', 'stop', 'flush'] as const; diff --git a/packages/integration-shims/src/SpanStreaming.ts b/packages/integration-shims/src/SpanStreaming.ts index 7b445f086145..269770b9ab21 100644 --- a/packages/integration-shims/src/SpanStreaming.ts +++ b/packages/integration-shims/src/SpanStreaming.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, defineIntegration } from '@sentry/core'; +import { consoleSandbox, defineIntegration } from '@sentry/core/browser'; /** * This is a shim for the SpanStreaming integration. diff --git a/packages/integration-shims/src/launchDarkly.ts b/packages/integration-shims/src/launchDarkly.ts index 76750f5c863c..08203b6a74ad 100644 --- a/packages/integration-shims/src/launchDarkly.ts +++ b/packages/integration-shims/src/launchDarkly.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core/browser'; import { FAKE_FUNCTION } from './common'; /** diff --git a/packages/integration-shims/src/logs.ts b/packages/integration-shims/src/logs.ts index 33af020efefc..7544a80cd30d 100644 --- a/packages/integration-shims/src/logs.ts +++ b/packages/integration-shims/src/logs.ts @@ -1,5 +1,5 @@ -import type { Integration, ParameterizedString } from '@sentry/core'; -import { consoleSandbox, defineIntegration } from '@sentry/core'; +import type { Integration, ParameterizedString } from '@sentry/core/browser'; +import { consoleSandbox, defineIntegration } from '@sentry/core/browser'; import { FAKE_FUNCTION } from './common'; import { DEBUG_BUILD } from './debug-build'; diff --git a/packages/integration-shims/tsconfig.json b/packages/integration-shims/tsconfig.json index bf45a09f2d71..d1a9c722679f 100644 --- a/packages/integration-shims/tsconfig.json +++ b/packages/integration-shims/tsconfig.json @@ -4,6 +4,7 @@ "include": ["src/**/*"], "compilerOptions": { + "moduleResolution": "bundler" // package-specific options } } diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 50f914bd168f..638dc7aa5a56 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -102,7 +102,7 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "ts-node scripts/buildRollup.ts", + "build:transpile": "ts-node --project tsconfig.tsnode.json scripts/buildRollup.ts", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", diff --git a/packages/nextjs/tsconfig.json b/packages/nextjs/tsconfig.json index bf45a09f2d71..202590772b10 100644 --- a/packages/nextjs/tsconfig.json +++ b/packages/nextjs/tsconfig.json @@ -5,5 +5,7 @@ "compilerOptions": { // package-specific options + "module": "esnext", + "moduleResolution": "bundler" } } diff --git a/packages/nextjs/tsconfig.tsnode.json b/packages/nextjs/tsconfig.tsnode.json new file mode 100644 index 000000000000..bf45a09f2d71 --- /dev/null +++ b/packages/nextjs/tsconfig.tsnode.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + // package-specific options + } +} diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts index c10cebe4ecf4..ce6d7a9cd2d5 100644 --- a/packages/react/src/error.ts +++ b/packages/react/src/error.ts @@ -1,5 +1,5 @@ import { captureException, withScope } from '@sentry/browser'; -import { isError } from '@sentry/core'; +import { isError } from '@sentry/core/browser'; import type { ErrorInfo } from 'react'; import { version } from 'react'; diff --git a/packages/react/src/reactrouter-compat-utils/route-manifest.ts b/packages/react/src/reactrouter-compat-utils/route-manifest.ts index bdc49f76705e..3f8c83a27c94 100644 --- a/packages/react/src/reactrouter-compat-utils/route-manifest.ts +++ b/packages/react/src/reactrouter-compat-utils/route-manifest.ts @@ -1,4 +1,4 @@ -import { debug } from '@sentry/core'; +import { debug } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; /** diff --git a/packages/react/src/reactrouter-compat-utils/utils.ts b/packages/react/src/reactrouter-compat-utils/utils.ts index 11a6d3609eaa..a43288823070 100644 --- a/packages/react/src/reactrouter-compat-utils/utils.ts +++ b/packages/react/src/reactrouter-compat-utils/utils.ts @@ -1,5 +1,5 @@ -import type { Span, TransactionSource } from '@sentry/core'; -import { debug, getActiveSpan, getRootSpan, spanToJSON } from '@sentry/core'; +import type { Span, TransactionSource } from '@sentry/core/browser'; +import { debug, getActiveSpan, getRootSpan, spanToJSON } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build'; import type { Location, MatchRoutes, RouteMatch, RouteObject } from '../types'; import { matchRouteManifest, stripBasenameFromPathname } from './route-manifest'; diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index f49948a2a74b..cdf97c02d03b 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -4,12 +4,12 @@ import { startBrowserTracingPageLoadSpan, WINDOW, } from '@sentry/browser'; -import type { Integration, TransactionSource } from '@sentry/core'; +import type { Integration, TransactionSource } from '@sentry/core/browser'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { Location } from './types'; // Many of the types below had to be mocked out to prevent typescript issues diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index b04510a68cc9..00f556f52cd7 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Scope } from '@sentry/core'; +import type { Scope } from '@sentry/core/browser'; import { addBreadcrumb, getClient, getCurrentScope, getGlobalScope, setNormalizationDepthOverrideHint, -} from '@sentry/core'; +} from '@sentry/core/browser'; interface Action { type: T; diff --git a/packages/react/src/sdk.ts b/packages/react/src/sdk.ts index 844bc30f1785..03981effc147 100644 --- a/packages/react/src/sdk.ts +++ b/packages/react/src/sdk.ts @@ -1,7 +1,7 @@ import type { BrowserOptions } from '@sentry/browser'; import { init as browserInit, setContext } from '@sentry/browser'; -import type { Client } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import type { Client } from '@sentry/core/browser'; +import { applySdkMetadata } from '@sentry/core/browser'; import { version } from 'react'; /** diff --git a/packages/react/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index d2424697d9d5..54b24b3d8490 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -4,12 +4,12 @@ import { startBrowserTracingPageLoadSpan, WINDOW, } from '@sentry/browser'; -import type { Integration } from '@sentry/core'; +import type { Integration } from '@sentry/core/browser'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, -} from '@sentry/core'; +} from '@sentry/core/browser'; import type { VendoredTanstackRouter, VendoredTanstackRouterRouteMatch } from './vendor/tanstackrouter-types'; /** diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 1e88479ffffe..5150975212ee 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -1,8 +1,8 @@ /** * @vitest-environment jsdom */ -import type { StartSpanOptions } from '@sentry/core'; -import { SentrySpan } from '@sentry/core'; +import type { StartSpanOptions } from '@sentry/core/browser'; +import { SentrySpan } from '@sentry/core/browser'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import * as React from 'react'; diff --git a/packages/react/test/reactrouter-cross-usage.test.tsx b/packages/react/test/reactrouter-cross-usage.test.tsx index c158f831c381..a5c797af8bc9 100644 --- a/packages/react/test/reactrouter-cross-usage.test.tsx +++ b/packages/react/test/reactrouter-cross-usage.test.tsx @@ -63,7 +63,7 @@ vi.mock('@sentry/browser', async requireActual => { }; }); -vi.mock('@sentry/core', async requireActual => { +async function coreMock(requireActual: () => Promise) { const actual = (await requireActual()) as any; return { ...actual, @@ -81,7 +81,10 @@ vi.mock('@sentry/core', async requireActual => { return span; }, }; -}); +} + +vi.mock('@sentry/core', coreMock); +vi.mock('@sentry/core/browser', coreMock); describe('React Router cross usage of wrappers', () => { function createMockBrowserClient(): BrowserClient { diff --git a/packages/react/test/reactrouter-descendant-routes.test.tsx b/packages/react/test/reactrouter-descendant-routes.test.tsx index a08893694a30..1172ee8d5d3e 100644 --- a/packages/react/test/reactrouter-descendant-routes.test.tsx +++ b/packages/react/test/reactrouter-descendant-routes.test.tsx @@ -58,14 +58,17 @@ vi.mock('@sentry/browser', async requireActual => { }; }); -vi.mock('@sentry/core', async requireActual => { +async function coreMock(requireActual: () => Promise) { return { ...(await requireActual()), getRootSpan: () => { return mockRootSpan; }, }; -}); +} + +vi.mock('@sentry/core', coreMock); +vi.mock('@sentry/core/browser', coreMock); describe('React Router Descendant Routes', () => { function createMockBrowserClient(): BrowserClient { diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index fda5043d2e6a..d439e76697d7 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -62,14 +62,17 @@ vi.mock('@sentry/browser', async requireActual => { }; }); -vi.mock('@sentry/core', async requireActual => { +async function coreMock(requireActual: () => Promise) { return { ...(await requireActual()), getRootSpan: () => { return mockRootSpan; }, }; -}); +} + +vi.mock('@sentry/core', coreMock); +vi.mock('@sentry/core/browser', coreMock); describe('reactRouterV6BrowserTracingIntegration', () => { function createMockBrowserClient(): BrowserClient { diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 60411418ca6a..09bcfd6ec9c2 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/browser'; -import * as SentryCore from '@sentry/core'; +import * as SentryCore from '@sentry/core/browser'; import * as Redux from 'redux'; import type { Mock } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -8,7 +8,7 @@ import { createReduxEnhancer } from '../src/redux'; const mockSetContext = vi.fn(); const mockGlobalScopeAddEventProcessor = vi.fn(); -vi.mock('@sentry/core', async requireActual => ({ +vi.mock('@sentry/core/browser', async requireActual => ({ ...(await requireActual()), getCurrentScope() { return { diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 41ff3c42258e..1b28c0b04eb9 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -5,6 +5,8 @@ "compilerOptions": { "lib": ["DOM", "es2020"], + "module": "esnext", + "moduleResolution": "bundler", // package-specific options "esModuleInterop": true, "jsx": "react" diff --git a/packages/remix/tsconfig.json b/packages/remix/tsconfig.json index f1f9d9ccc513..a1752fde9b92 100644 --- a/packages/remix/tsconfig.json +++ b/packages/remix/tsconfig.json @@ -5,6 +5,7 @@ "compilerOptions": { "jsx": "react", - "module": "es2020" + "module": "esnext", + "moduleResolution": "bundler" } } diff --git a/packages/solidstart/test/server/errorboundary.test.tsx b/packages/solidstart/test/server/errorboundary.test.tsx index 3ed39bbfea13..7d10e3a14e1e 100644 --- a/packages/solidstart/test/server/errorboundary.test.tsx +++ b/packages/solidstart/test/server/errorboundary.test.tsx @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ +import type * as SentryCoreBrowser from '@sentry/core/browser'; import type * as SentryCore from '@sentry/core'; + import { createTransport, getCurrentScope, setCurrentClient } from '@sentry/core'; import { render } from '@solidjs/testing-library'; import userEvent from '@testing-library/user-event'; @@ -7,14 +9,17 @@ import { ErrorBoundary } from 'solid-js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { NodeClient, withSentryErrorBoundary } from '../../src/server'; +// mock both old combined @sentry/core and @sentry/core/browser to be safe. const mockCaptureException = vi.fn(); -vi.mock('@sentry/core', async () => { - const actual = await vi.importActual('@sentry/core'); +async function mockCore(importActual: () => Promise) { + const actual = await importActual(); return { ...actual, captureException: (...args) => mockCaptureException(...args), } as typeof SentryCore; -}); +} +vi.mock('@sentry/core/browser', mockCore); +vi.mock('@sentry/core', mockCore); const user = userEvent.setup(); const SentryErrorBoundary = withSentryErrorBoundary(ErrorBoundary); diff --git a/packages/solidstart/tsconfig.json b/packages/solidstart/tsconfig.json index b0eb9ecb6476..78a97ddc222b 100644 --- a/packages/solidstart/tsconfig.json +++ b/packages/solidstart/tsconfig.json @@ -3,5 +3,8 @@ "include": ["src/**/*"], - "compilerOptions": {} + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler" + } } diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 26872a0f6f24..c0e33c49c031 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -128,7 +128,6 @@ async function instrumentHandle( // to avoid doing the dynamic import on every request if (options.injectFetchProxyScript == null) { try { - // @ts-expect-error - the dynamic import is fine here const { VERSION } = await import('@sveltejs/kit'); options.injectFetchProxyScript = isFetchProxyRequired(VERSION); } catch { diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index ae0a29a25243..b8f439092bee 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -39,7 +39,6 @@ export async function loadSvelteConfig(): Promise Date: Mon, 11 May 2026 11:36:09 +0900 Subject: [PATCH 07/32] feat(deps): Bump hono from 4.12.16 to 4.12.18 (#20777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.16 to 4.12.18.
Release notes

Sourced from hono's releases.

v4.12.18

Security fixes

This release includes fixes for the following security issues:

Cache Middleware ignores Vary: Authorization / Vary: Cookie leading to cross-user cache leakage

Affects: Cache Middleware. Fixes missing cache-skip handling for Vary: Authorization and Vary: Cookie, where a response cached for one authenticated user could be served to other users. GHSA-p77w-8qqv-26rm

CSS Declaration Injection via Style Object Values in JSX SSR

Affects: hono/jsx. Fixes a missing CSS-context escape for style object values and property names, where untrusted input could inject additional CSS declarations. The impact is limited to CSS and does not allow JavaScript execution. GHSA-qp7p-654g-cw7p

Improper validation of NumericDate claims (exp, nbf, iat) in JWT verify()

Affects: hono/utils/jwt. Fixes improper validation of exp, nbf, and iat claims, where falsy, non-finite, or non-numeric values could silently bypass time-based checks instead of being rejected per RFC 7519. GHSA-hm8q-7f3q-5f36


Users who use the JWT helper, hono/jsx, or the Cache middleware are strongly encouraged to upgrade to this version.

v4.12.17

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.12.16...v4.12.17

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.12.16&new-version=4.12.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/bun-integration-tests/package.json | 2 +- dev-packages/cloudflare-integration-tests/package.json | 2 +- dev-packages/node-integration-tests/package.json | 2 +- yarn.lock | 9 +++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dev-packages/bun-integration-tests/package.json b/dev-packages/bun-integration-tests/package.json index 9e44c9c60394..75c34603ce71 100644 --- a/dev-packages/bun-integration-tests/package.json +++ b/dev-packages/bun-integration-tests/package.json @@ -15,7 +15,7 @@ "dependencies": { "@sentry/bun": "10.52.0", "@sentry/hono": "10.52.0", - "hono": "^4.12.16" + "hono": "^4.12.18" }, "devDependencies": { "@sentry-internal/test-utils": "10.52.0", diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index 9aaf44103ed9..15e41ec37f62 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -16,7 +16,7 @@ "@langchain/langgraph": "^1.0.1", "@sentry/cloudflare": "10.52.0", "@sentry/hono": "10.52.0", - "hono": "^4.12.16" + "hono": "^4.12.18" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250922.0", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index bfa5e3ea85bd..a8aa41b3c6a9 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -58,7 +58,7 @@ "generic-pool": "^3.9.0", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", - "hono": "^4.12.16", + "hono": "^4.12.18", "http-terminator": "^3.2.0", "ioredis": "^5.4.1", "kafkajs": "2.2.4", diff --git a/yarn.lock b/yarn.lock index 16aa3ea14f82..84bbaea82771 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18864,10 +18864,10 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hono@^4.12.16: - version "4.12.16" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.16.tgz#90fcc63caa713199703bda08d8518f654e98b516" - integrity sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg== +hono@^4.12.18: + version "4.12.18" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.18.tgz#f6d301938868c3a8bdb639495f4e326a19181505" + integrity sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ== hookable@^5.5.3: version "5.5.3" @@ -28493,6 +28493,7 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" + uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From 0b5cc47331c3d54625fa6b95f9f9a6b13e7d76b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 13:54:53 +0900 Subject: [PATCH 08/32] chore(deps): Bump rollup from 4.59.0 to 4.60.3 (#20716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [rollup](https://github.com/rollup/rollup) from 4.59.0 to 4.60.3.
Release notes

Sourced from rollup's releases.

v4.60.2

4.60.2

2026-04-18

Bug Fixes

  • Resolve a variable rendering bug when generating different formats from the same build (#6350)

Pull Requests

v4.60.1

4.60.1

2026-03-30

Bug Fixes

  • Resolve a situation where side effect imports could be dropped due to a caching issue (#6286)

Pull Requests

... (truncated)

Changelog

Sourced from rollup's changelog.

4.60.3

2026-05-04

Bug Fixes

  • Ensure nested "exports" variables are not renamed (#6360)

Pull Requests

4.60.2

2026-04-18

Bug Fixes

  • Resolve a variable rendering bug when generating different formats from the same build (#6350)

Pull Requests

4.60.1

2026-03-30

... (truncated)

Commits
  • b47bdab 4.60.3
  • 15c5f33 Add again some unneeded dev dependencies, to make some builds succeed
  • 12195dc fix: do not rename nested "exports" bindings that do not conflict (#6360)
  • b74aa39 Migrate instructions to AGENTS.md
  • aa5a377 fix(deps): update minor/patch updates (#6365)
  • 197e68b chore(deps): update msys2/setup-msys2 digest to e989830 (#6364)
  • cded70a fix(deps): update swc monorepo (major) (#6366)
  • bb2b8a5 docs: add missing backticks in plugin-development (#6368)
  • 20af1c4 chore(deps): lock file maintenance (#6367)
  • a6be82b 4.60.2
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/bundler-tests/package.json | 2 +- package.json | 2 +- packages/nextjs/package.json | 2 +- yarn.lock | 306 ++++++++++++------------ 4 files changed, 156 insertions(+), 156 deletions(-) diff --git a/dev-packages/bundler-tests/package.json b/dev-packages/bundler-tests/package.json index b85893a20a50..dce2d1a4886c 100644 --- a/dev-packages/bundler-tests/package.json +++ b/dev-packages/bundler-tests/package.json @@ -14,7 +14,7 @@ "dependencies": { "@rollup/plugin-node-resolve": "^16.0.3", "@sentry/browser": "10.52.0", - "rollup": "^4.0.0", + "rollup": "^4.60.3", "vite": "^5.0.0", "vitest": "^3.2.4", "webpack": "^5.0.0" diff --git a/package.json b/package.json index e71f94772bd0..bca64a9f863f 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "oxlint": "^1.53.0", "oxlint-tsgolint": "^0.16.0", "rimraf": "^5.0.10", - "rollup": "^4.59.0", + "rollup": "^4.60.3", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^3.7.1", "size-limit": "~12.1.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 638dc7aa5a56..f8c1126d0041 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -87,7 +87,7 @@ "@sentry/react": "10.52.0", "@sentry/vercel-edge": "10.52.0", "@sentry/webpack-plugin": "^5.2.0", - "rollup": "^4.35.0", + "rollup": "^4.60.3", "stacktrace-parser": "^0.1.11" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 84bbaea82771..d5e66586670c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7542,130 +7542,130 @@ estree-walker "^2.0.2" picomatch "^4.0.2" -"@rollup/rollup-android-arm-eabi@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz#a6742c74c7d9d6d604ef8a48f99326b4ecda3d82" - integrity sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg== - -"@rollup/rollup-android-arm64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz#97247be098de4df0c11971089fd2edf80a5da8cf" - integrity sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q== - -"@rollup/rollup-darwin-arm64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz#674852cf14cf11b8056e0b1a2f4e872b523576cf" - integrity sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg== - -"@rollup/rollup-darwin-x64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz#36dfd7ed0aaf4d9d89d9ef983af72632455b0246" - integrity sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w== - -"@rollup/rollup-freebsd-arm64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz#2f87c2074b4220260fdb52a9996246edfc633c22" - integrity sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA== - -"@rollup/rollup-freebsd-x64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz#9b5a26522a38a95dc06616d1939d4d9a76937803" - integrity sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg== - -"@rollup/rollup-linux-arm-gnueabihf@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz#86aa4859385a8734235b5e40a48e52d770758c3a" - integrity sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw== - -"@rollup/rollup-linux-arm-musleabihf@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz#cbe70e56e6ece8dac83eb773b624fc9e5a460976" - integrity sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA== - -"@rollup/rollup-linux-arm64-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz#d14992a2e653bc3263d284bc6579b7a2890e1c45" - integrity sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA== - -"@rollup/rollup-linux-arm64-musl@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz#2fdd1ddc434ea90aeaa0851d2044789b4d07f6da" - integrity sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA== - -"@rollup/rollup-linux-loong64-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz#8a181e6f89f969f21666a743cd411416c80099e7" - integrity sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg== - -"@rollup/rollup-linux-loong64-musl@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz#904125af2babc395f8061daa27b5af1f4e3f2f78" - integrity sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q== - -"@rollup/rollup-linux-ppc64-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz#a57970ac6864c9a3447411a658224bdcf948be22" - integrity sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA== - -"@rollup/rollup-linux-ppc64-musl@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz#bb84de5b26870567a4267666e08891e80bb56a63" - integrity sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA== - -"@rollup/rollup-linux-riscv64-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz#72d00d2c7fb375ce3564e759db33f17a35bffab9" - integrity sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg== - -"@rollup/rollup-linux-riscv64-musl@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz#4c166ef58e718f9245bd31873384ba15a5c1a883" - integrity sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg== - -"@rollup/rollup-linux-s390x-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz#bb5025cde9a61db478c2ca7215808ad3bce73a09" - integrity sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w== - -"@rollup/rollup-linux-x64-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz#9b66b1f9cd95c6624c788f021c756269ffed1552" - integrity sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg== - -"@rollup/rollup-linux-x64-musl@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz#b007ca255dc7166017d57d7d2451963f0bd23fd9" - integrity sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg== - -"@rollup/rollup-openbsd-x64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz#e8b357b2d1aa2c8d76a98f5f0d889eabe93f4ef9" - integrity sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ== - -"@rollup/rollup-openharmony-arm64@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz#96c2e3f4aacd3d921981329831ff8dde492204dc" - integrity sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA== - -"@rollup/rollup-win32-arm64-msvc@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz#2d865149d706d938df8b4b8f117e69a77646d581" - integrity sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A== - -"@rollup/rollup-win32-ia32-msvc@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz#abe1593be0fa92325e9971c8da429c5e05b92c36" - integrity sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA== - -"@rollup/rollup-win32-x64-gnu@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz#c4af3e9518c9a5cd4b1c163dc81d0ad4d82e7eab" - integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA== - -"@rollup/rollup-win32-x64-msvc@4.59.0": - version "4.59.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c" - integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== +"@rollup/rollup-android-arm-eabi@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz#31503ca40424374cd6c5198031cf4d5a73de9727" + integrity sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw== + +"@rollup/rollup-android-arm64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz#7cbc30c88507013d0f982cfeb8884337ba1e0bb2" + integrity sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw== + +"@rollup/rollup-darwin-arm64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz#bc341a93bb2111326a2865f55d1d23baedecf40c" + integrity sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g== + +"@rollup/rollup-darwin-x64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz#dfa0236581c55ecc0bcaeb2ea1f2e800c58dc3e2" + integrity sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw== + +"@rollup/rollup-freebsd-arm64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz#4c5977413b87808a13b5edd524e46fafddb85b52" + integrity sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ== + +"@rollup/rollup-freebsd-x64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz#5cb2cee62ffee3ada4a0b44353e96cf98cfc7c3c" + integrity sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA== + +"@rollup/rollup-linux-arm-gnueabihf@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz#04700cad36dd43ae81044fe7ee73e925845c4b85" + integrity sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g== + +"@rollup/rollup-linux-arm-musleabihf@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz#548ebf3997b3a6dcc7cdd7da813ff0c46000ac0a" + integrity sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w== + +"@rollup/rollup-linux-arm64-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz#0264608f504b33725639ebe93be02c40e71a35c1" + integrity sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA== + +"@rollup/rollup-linux-arm64-musl@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz#147cf4889502cd3b331a800b8ca6741f87873079" + integrity sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg== + +"@rollup/rollup-linux-loong64-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz#0c27c6b5258dcb3d0290e3bd04ba6277c9d7e541" + integrity sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA== + +"@rollup/rollup-linux-loong64-musl@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz#f0f18075ea0bfa2c992f8e3933b39b6ef91f7799" + integrity sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg== + +"@rollup/rollup-linux-ppc64-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz#149bb5cb8893589ffaa1924b4eac4282e9fa4c69" + integrity sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ== + +"@rollup/rollup-linux-ppc64-musl@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz#200a063e298b05f996917d2aa53de749d54c0ca0" + integrity sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA== + +"@rollup/rollup-linux-riscv64-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz#6d6d6eb996197ba86f95f9a6c442bc862f0756d4" + integrity sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw== + +"@rollup/rollup-linux-riscv64-musl@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz#9deb86001785cfcbc761457f50cd7c112fda0df9" + integrity sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ== + +"@rollup/rollup-linux-s390x-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz#d8228720c6e42da190d96c31a3495d70cf8284b9" + integrity sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig== + +"@rollup/rollup-linux-x64-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz#df6bb38617a66a842bd2aeac9560cd729d084258" + integrity sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA== + +"@rollup/rollup-linux-x64-musl@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz#75e3e72849266b4fdd65f2da6c62423051e35636" + integrity sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA== + +"@rollup/rollup-openbsd-x64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz#e1080f0efb8b15cda39b3e62de5fb806079ab6e9" + integrity sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q== + +"@rollup/rollup-openharmony-arm64@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz#1fbda2d95c29dbfceb62785431754cd5aab86c72" + integrity sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg== + +"@rollup/rollup-win32-arm64-msvc@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz#deab3470815f97996f1d0d3608549cf1b7e4ffc2" + integrity sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg== + +"@rollup/rollup-win32-ia32-msvc@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz#817acae2ed4572960b59235ff2322381b6d82f26" + integrity sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA== + +"@rollup/rollup-win32-x64-gnu@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz#48129be99b0250d76b9c6d0ac983bef563a1c48a" + integrity sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A== + +"@rollup/rollup-win32-x64-msvc@4.60.3": + version "4.60.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz#cc6f094a3ffe5556bb4a831ee6fb572b8cd81a75" + integrity sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA== "@rtsao/scc@^1.1.0": version "1.1.0" @@ -26786,38 +26786,38 @@ rollup@^3.27.1, rollup@^3.28.1: optionalDependencies: fsevents "~2.3.2" -rollup@^4.0.0, rollup@^4.20.0, rollup@^4.34.9, rollup@^4.35.0, rollup@^4.55.1, rollup@^4.59.0: - version "4.59.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f" - integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg== +rollup@^4.20.0, rollup@^4.34.9, rollup@^4.55.1, rollup@^4.60.3: + version "4.60.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.60.3.tgz#789258d41d090687d0ca7e80e8583d733711ddd3" + integrity sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A== dependencies: "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.59.0" - "@rollup/rollup-android-arm64" "4.59.0" - "@rollup/rollup-darwin-arm64" "4.59.0" - "@rollup/rollup-darwin-x64" "4.59.0" - "@rollup/rollup-freebsd-arm64" "4.59.0" - "@rollup/rollup-freebsd-x64" "4.59.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" - "@rollup/rollup-linux-arm-musleabihf" "4.59.0" - "@rollup/rollup-linux-arm64-gnu" "4.59.0" - "@rollup/rollup-linux-arm64-musl" "4.59.0" - "@rollup/rollup-linux-loong64-gnu" "4.59.0" - "@rollup/rollup-linux-loong64-musl" "4.59.0" - "@rollup/rollup-linux-ppc64-gnu" "4.59.0" - "@rollup/rollup-linux-ppc64-musl" "4.59.0" - "@rollup/rollup-linux-riscv64-gnu" "4.59.0" - "@rollup/rollup-linux-riscv64-musl" "4.59.0" - "@rollup/rollup-linux-s390x-gnu" "4.59.0" - "@rollup/rollup-linux-x64-gnu" "4.59.0" - "@rollup/rollup-linux-x64-musl" "4.59.0" - "@rollup/rollup-openbsd-x64" "4.59.0" - "@rollup/rollup-openharmony-arm64" "4.59.0" - "@rollup/rollup-win32-arm64-msvc" "4.59.0" - "@rollup/rollup-win32-ia32-msvc" "4.59.0" - "@rollup/rollup-win32-x64-gnu" "4.59.0" - "@rollup/rollup-win32-x64-msvc" "4.59.0" + "@rollup/rollup-android-arm-eabi" "4.60.3" + "@rollup/rollup-android-arm64" "4.60.3" + "@rollup/rollup-darwin-arm64" "4.60.3" + "@rollup/rollup-darwin-x64" "4.60.3" + "@rollup/rollup-freebsd-arm64" "4.60.3" + "@rollup/rollup-freebsd-x64" "4.60.3" + "@rollup/rollup-linux-arm-gnueabihf" "4.60.3" + "@rollup/rollup-linux-arm-musleabihf" "4.60.3" + "@rollup/rollup-linux-arm64-gnu" "4.60.3" + "@rollup/rollup-linux-arm64-musl" "4.60.3" + "@rollup/rollup-linux-loong64-gnu" "4.60.3" + "@rollup/rollup-linux-loong64-musl" "4.60.3" + "@rollup/rollup-linux-ppc64-gnu" "4.60.3" + "@rollup/rollup-linux-ppc64-musl" "4.60.3" + "@rollup/rollup-linux-riscv64-gnu" "4.60.3" + "@rollup/rollup-linux-riscv64-musl" "4.60.3" + "@rollup/rollup-linux-s390x-gnu" "4.60.3" + "@rollup/rollup-linux-x64-gnu" "4.60.3" + "@rollup/rollup-linux-x64-musl" "4.60.3" + "@rollup/rollup-openbsd-x64" "4.60.3" + "@rollup/rollup-openharmony-arm64" "4.60.3" + "@rollup/rollup-win32-arm64-msvc" "4.60.3" + "@rollup/rollup-win32-ia32-msvc" "4.60.3" + "@rollup/rollup-win32-x64-gnu" "4.60.3" + "@rollup/rollup-win32-x64-msvc" "4.60.3" fsevents "~2.3.2" rou3@^0.7.12: From e9791d35adb995bc4c6222457f318a79b535b04f Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 14:28:36 +0900 Subject: [PATCH 09/32] chore: Add external contributor to CHANGELOG.md (#20780) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #20719 Co-authored-by: isaacs <9287+isaacs@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f23fe92ce89b..a8fa4804fef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @dmmulroy. Thank you for your contribution! + ## 10.52.0 ### Important Changes From 862359b5f26efd30e230d2891fa9b458b6931d88 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 11 May 2026 09:57:12 +0200 Subject: [PATCH 10/32] fix(replay): Capture aborted/errored fetch requests in replay network tab (#20722) Fetch requests that are aborted or fail before receiving response headers never appear in the replay network tab. Reproducible by calling `fetch()` with an `AbortController` that fires before headers arrive. With this setup the request shows up in Sentry breadcrumbs (on the error objects) but is missing from the replay network tab. `_isFetchHint()` gates on `hint.response` existing, but when a fetch errors or is aborted the breadcrumb handler in the breadcrumb integration creates a hint with the error as `data` and no `response` (since none was received). Changed the guard to check `hint.input` instead, which is always present. Closes https://github.com/getsentry/sentry-javascript/issues/20714 Co-authored-by: Claude Opus 4.6 (1M context) --- .../coreHandlers/handleNetworkBreadcrumbs.ts | 4 +- .../handleNetworkBreadcrumbs.test.ts | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts index 6a2e49bfa5b9..29dc5ffd9721 100644 --- a/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay-internal/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -95,6 +95,6 @@ function _isXhrHint(hint?: BreadcrumbHint): hint is XhrHint { return hint?.xhr; } -function _isFetchHint(hint?: BreadcrumbHint): hint is FetchHint { - return hint?.response; +function _isFetchHint(hint?: BreadcrumbHint): hint is Partial { + return hint?.input !== undefined; } diff --git a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index b88b4d1a2358..ddc5a8d7ff6d 100644 --- a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -359,6 +359,56 @@ other-header: test`; ]); }); + it('handles fetch breadcrumb for aborted request (no response)', async () => { + const breadcrumb: Breadcrumb = { + category: 'fetch', + level: 'error', + data: { + method: 'GET', + url: 'https://example.com', + }, + }; + + const hint: FetchBreadcrumbHint = { + data: new Error('The operation was aborted'), + input: ['GET', {}], + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'fetch', + level: 'error', + data: { + method: 'GET', + url: 'https://example.com', + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + statusCode: 0, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.fetch', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + it('parses fetch response body if necessary', async () => { const breadcrumb: Breadcrumb = { category: 'fetch', From c259c7568417c0e3fa0afa230695bd43d2d54f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Mon, 11 May 2026 11:48:07 +0200 Subject: [PATCH 11/32] fix(mcp): retroactively wrap handlers registered before wrapMcpServerWithSentry (#20699) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `wrapMcpServerWithSentry` works by patching the registration methods (`registerTool`, `registerResource`, `registerPrompt`) so that any handler passed to them gets wrapped with Sentry error capture. The problem: if tools are registered *before* wrapping, those already-stored handlers are never touched, so errors from them are silently dropped. This fix adds a retroactive pass (`wrapExistingHandlers`) that runs immediately after the registration methods are patched. It walks the McpServer's internal registries (`_registeredTools`, `_registeredResources`, `_registeredResourceTemplates`, `_registeredPrompts`) and wraps the stored callables in-place — `executor` for tools, `readCallback` for resources and templates, `handler` for prompts. These are the properties the MCP SDK reads at call time (not captured by closure at registration), so replacing them is equivalent to having wrapped the original call. Both orderings are now fully supported. Wrapping at construction is still recommended for consistency with how other SDK integrations work, but it's not required. **Recommended (wrap at construction):** ```ts const server = Sentry.wrapMcpServerWithSentry( new McpServer({ name: 'my-server', version: '1.0.0' }), ); server.registerTool('my-tool', schema, handler); ``` **Also works (wrap after registration):** ```ts const server = new McpServer({ name: 'my-server', version: '1.0.0' }); server.registerTool('my-tool', schema, handler); Sentry.wrapMcpServerWithSentry(server); // retroactively wraps the already-registered tool ``` The retroactive wrapping intentionally accesses private MCP SDK internals. The JSDoc on `wrapExistingHandlers` includes a pinned upstream source link and a note to re-verify the internal shapes when upgrading the MCP SDK — all access is defensive and skips silently if a property isn't found. ## Checklist - [x] Tests added (`Retroactive handler wrapping` describe block in `mcpServerWrapper.test.ts`) - [x] No lint changes needed (pure TS, no new deps) - [ ] Link an issue if there is one related to your pull request. If no issue is linked, one will be auto-generated and linked. Closes #issue_link_here Co-authored-by: Claude Sonnet 4.6 (1M context) --- .../src/integrations/mcp-server/handlers.ts | 71 +++++++++++++++++++ .../core/src/integrations/mcp-server/index.ts | 13 +++- .../mcp-server/mcpServerWrapper.test.ts | 41 ++++++++++- .../lib/integrations/mcp-server/testUtils.ts | 35 +++++++++ 4 files changed, 157 insertions(+), 3 deletions(-) diff --git a/packages/core/src/integrations/mcp-server/handlers.ts b/packages/core/src/integrations/mcp-server/handlers.ts index 5ac0d0e0722a..200156a24093 100644 --- a/packages/core/src/integrations/mcp-server/handlers.ts +++ b/packages/core/src/integrations/mcp-server/handlers.ts @@ -167,3 +167,74 @@ export function wrapAllMCPHandlers(serverInstance: MCPServerInstance): void { wrapResourceHandlers(serverInstance); wrapPromptHandlers(serverInstance); } + +/** + * Retroactively wraps handlers on tools, resources, and prompts that were registered + * before `wrapMcpServerWithSentry` was called. + * + * The MCP SDK stores registered entries in private maps and invokes them via the entry's + * own property at call time — `executor` for tools, `readCallback` for resources, and + * `handler` for prompts. Replacing those properties + * in-place is therefore equivalent to having wrapped the original registration call. + * + * NOTE: This intentionally accesses private MCP SDK internals (`_registeredTools` etc.). + * The properties and their shapes are verified against @modelcontextprotocol/sdk source: + * https://github.com/modelcontextprotocol/typescript-sdk/blob/2c0c481cb9dbfd15c8613f765c940a5f5bace94d/packages/server/src/server/mcp.ts#L304 + * When upgrading the MCP SDK, re-verify that these internal maps and their callable + * properties still exist and are invoked directly (not captured by closure at registration). + * All access is defensive — if a property is absent or not a function we skip silently. + * @internal + */ +export function wrapExistingHandlers(serverInstance: MCPServerInstance): void { + const server = serverInstance as unknown as Record; + + // Tools: MCP SDK calls registeredTool.executor (generated from handler at registration time) + const registeredTools = server['_registeredTools']; + if (registeredTools && typeof registeredTools === 'object') { + for (const [name, tool] of Object.entries(registeredTools as Record>)) { + if (typeof tool['executor'] === 'function') { + tool['executor'] = createWrappedHandler(tool['executor'] as MCPHandler, 'registerTool', name); + } + } + } + + // Resources: MCP SDK calls registeredResource.readCallback + const registeredResources = server['_registeredResources']; + if (registeredResources && typeof registeredResources === 'object') { + for (const [name, resource] of Object.entries(registeredResources as Record>)) { + if (typeof resource['readCallback'] === 'function') { + resource['readCallback'] = createWrappedHandler( + resource['readCallback'] as MCPHandler, + 'registerResource', + name, + ); + } + } + } + + // Resource templates: MCP SDK calls registeredResourceTemplate.readCallback + const registeredResourceTemplates = server['_registeredResourceTemplates']; + if (registeredResourceTemplates && typeof registeredResourceTemplates === 'object') { + for (const [name, template] of Object.entries( + registeredResourceTemplates as Record>, + )) { + if (typeof template['readCallback'] === 'function') { + template['readCallback'] = createWrappedHandler( + template['readCallback'] as MCPHandler, + 'registerResource', + name, + ); + } + } + } + + // Prompts: MCP SDK calls registeredPrompt.handler + const registeredPrompts = server['_registeredPrompts']; + if (registeredPrompts && typeof registeredPrompts === 'object') { + for (const [name, prompt] of Object.entries(registeredPrompts as Record>)) { + if (typeof prompt['handler'] === 'function') { + prompt['handler'] = createWrappedHandler(prompt['handler'] as MCPHandler, 'registerPrompt', name); + } + } + } +} diff --git a/packages/core/src/integrations/mcp-server/index.ts b/packages/core/src/integrations/mcp-server/index.ts index b4ef87f0fa0a..952e780c0a9f 100644 --- a/packages/core/src/integrations/mcp-server/index.ts +++ b/packages/core/src/integrations/mcp-server/index.ts @@ -1,6 +1,6 @@ import { getClient } from '../../currentScopes'; import { fill } from '../../utils/object'; -import { wrapAllMCPHandlers } from './handlers'; +import { wrapAllMCPHandlers, wrapExistingHandlers } from './handlers'; import { wrapTransportError, wrapTransportOnClose, wrapTransportOnMessage, wrapTransportSend } from './transport'; import type { MCPServerInstance, McpServerWrapperOptions, MCPTransport, ResolvedMcpOptions } from './types'; import { validateMcpServerInstance } from './validation'; @@ -18,17 +18,24 @@ const wrappedMcpServerInstances = new WeakSet(); * and versions that expose the newer `registerTool`/`registerResource`/`registerPrompt` API (introduced in 1.x, sole API in 2.x). * Automatically instruments transport methods and handler functions for comprehensive monitoring. * + * Both call orderings are supported: wrapping before or after registering tools, resources, + * and prompts. Sentry patches the registration methods for future handlers and retroactively + * wraps any already-registered ones. Wrapping at construction time is recommended by + * convention (consistent with other SDK integrations), but is not required. + * * @example * ```typescript * import * as Sentry from '@sentry/core'; * import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; * import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; * - * // Default: inputs/outputs captured based on sendDefaultPii option + * // Wrap first, then register tools — this is the correct order * const server = Sentry.wrapMcpServerWithSentry( * new McpServer({ name: "my-server", version: "1.0.0" }) * ); * + * server.registerTool('my-tool', schema, handler); + * * // Explicitly control input/output capture * const server = Sentry.wrapMcpServerWithSentry( * new McpServer({ name: "my-server", version: "1.0.0" }), @@ -80,6 +87,8 @@ export function wrapMcpServerWithSentry(mcpServerInstance: S, wrapAllMCPHandlers(serverInstance); + wrapExistingHandlers(serverInstance); + wrappedMcpServerInstances.add(mcpServerInstance); return mcpServerInstance; } diff --git a/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts b/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts index 3fc48a2e0b47..845d7d4786da 100644 --- a/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts +++ b/packages/core/test/lib/integrations/mcp-server/mcpServerWrapper.test.ts @@ -2,7 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as currentScopes from '../../../../src/currentScopes'; import { wrapMcpServerWithSentry } from '../../../../src/integrations/mcp-server'; import * as tracingModule from '../../../../src/tracing'; -import { createMockMcpServer, createMockMcpServerWithRegisterApi } from './testUtils'; +import { + createMockMcpServer, + createMockMcpServerWithPreregisteredHandlers, + createMockMcpServerWithRegisterApi, +} from './testUtils'; describe('wrapMcpServerWithSentry', () => { const startSpanSpy = vi.spyOn(tracingModule, 'startSpan'); @@ -145,6 +149,41 @@ describe('wrapMcpServerWithSentry', () => { }); }); + describe('Retroactive handler wrapping (handlers registered before wrapMcpServerWithSentry)', () => { + it('should replace executor/readCallback/handler on pre-registered entries with wrapped versions', () => { + const server = createMockMcpServerWithPreregisteredHandlers(); + const { toolExecutor, resourceReadCallback, resourceTemplateReadCallback, promptHandler } = server._originals; + + wrapMcpServerWithSentry(server); + + expect(server._registeredTools['my-tool']!.executor).not.toBe(toolExecutor); + expect(server._registeredResources['res://my-resource']!.readCallback).not.toBe(resourceReadCallback); + expect(server._registeredResourceTemplates['my-template']!.readCallback).not.toBe(resourceTemplateReadCallback); + expect(server._registeredPrompts['my-prompt']!.handler).not.toBe(promptHandler); + }); + + it('should still wrap the registration methods for future handlers', () => { + const server = createMockMcpServerWithPreregisteredHandlers(); + const originalRegisterTool = server.registerTool; + + wrapMcpServerWithSentry(server); + + expect(server.registerTool).not.toBe(originalRegisterTool); + }); + + it('should not double-wrap if called twice on the same instance with pre-registered handlers', () => { + const server = createMockMcpServerWithPreregisteredHandlers(); + + wrapMcpServerWithSentry(server); + const executorAfterFirstWrap = server._registeredTools['my-tool']!.executor; + + wrapMcpServerWithSentry(server); + const executorAfterSecondWrap = server._registeredTools['my-tool']!.executor; + + expect(executorAfterFirstWrap).toBe(executorAfterSecondWrap); + }); + }); + describe('Handler Wrapping (register* API)', () => { let mockServer: ReturnType; let wrappedServer: ReturnType; diff --git a/packages/core/test/lib/integrations/mcp-server/testUtils.ts b/packages/core/test/lib/integrations/mcp-server/testUtils.ts index 23b9ee6ff51b..ebe940365d90 100644 --- a/packages/core/test/lib/integrations/mcp-server/testUtils.ts +++ b/packages/core/test/lib/integrations/mcp-server/testUtils.ts @@ -15,6 +15,41 @@ export function createMockMcpServer() { }; } +/** + * Create a mock MCP server that simulates already having tools/resources/prompts registered + * (i.e. wrapMcpServerWithSentry is called after registration). Mirrors the internal shape + * used by McpServer v2: tools have an `executor`, resources/prompts have `readCallback`/`handler`. + */ +export function createMockMcpServerWithPreregisteredHandlers() { + const toolExecutor = vi.fn().mockResolvedValue({ content: [] }); + const resourceReadCallback = vi.fn().mockResolvedValue({ contents: [] }); + const resourceTemplateReadCallback = vi.fn().mockResolvedValue({ contents: [] }); + const promptHandler = vi.fn().mockResolvedValue({ messages: [] }); + + return { + registerTool: vi.fn(), + registerResource: vi.fn(), + registerPrompt: vi.fn(), + connect: vi.fn().mockResolvedValue(undefined), + server: { setRequestHandler: vi.fn() }, + // Simulated internal registries (mirrors McpServer v2 private fields) + _registeredTools: { + 'my-tool': { executor: toolExecutor }, + }, + _registeredResources: { + 'res://my-resource': { readCallback: resourceReadCallback }, + }, + _registeredResourceTemplates: { + 'my-template': { readCallback: resourceTemplateReadCallback }, + }, + _registeredPrompts: { + 'my-prompt': { handler: promptHandler }, + }, + // Expose the original fns so tests can assert wrapping happened + _originals: { toolExecutor, resourceReadCallback, resourceTemplateReadCallback, promptHandler }, + }; +} + /** * Create a mock MCP server instance using the new register* API (SDK >=1.x / 2.x) */ From e2b9cdad12ffbd2526bdf9b0449ba214785a30ed Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 13:09:44 +0200 Subject: [PATCH 12/32] feat(core): Add `addConsoleInstrumentationFilter` utility (#20790) We want to leverage this for Node 26 to filter deprecation messages from IITM, but this could also be used by users if they want to silence certain things. Required for https://github.com/getsentry/sentry-javascript/pull/20710 --- .../console/filter/instrument.mjs | 10 ++ .../integrations/console/filter/scenario.mjs | 9 ++ .../integrations/console/filter/test.ts | 35 +++++++ packages/core/src/instrument/console.ts | 48 +++++++++- packages/core/src/integrations/console.ts | 13 ++- packages/core/src/shared-exports.ts | 2 +- packages/core/src/utils/string.ts | 11 ++- .../core/test/lib/instrument/console.test.ts | 92 ++++++++++++++++++- .../node-core/src/integrations/console.ts | 9 ++ 9 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs new file mode 100644 index 000000000000..16c05ae32ed0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + defaultIntegrations: false, + integrations: [Sentry.consoleIntegration({ filter: ['foo'] })], +}); diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs b/dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs new file mode 100644 index 000000000000..5d1e93582c0b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs @@ -0,0 +1,9 @@ +/* eslint-disable no-console */ +import * as Sentry from '@sentry/node'; + +console.log('hello'); +console.log('foo'); +console.log('foo2'); +console.log('baz'); + +Sentry.captureException(new Error('Test Error')); diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts b/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts new file mode 100644 index 000000000000..5f0bf9e8b14a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts @@ -0,0 +1,35 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('Console Integration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('filters console messages', async () => { + await createRunner() + .expect({ + event: { + exception: { + values: [ + { + value: 'Test Error', + }, + ], + }, + breadcrumbs: [ + expect.objectContaining({ + message: 'hello', + }), + expect.objectContaining({ + message: 'baz', + }), + ], + }, + }) + .start() + .completed(); + }); + }); +}); diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index ef7e9c804943..737e9fd907fa 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -1,10 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ +import { DEBUG_BUILD } from '../debug-build'; import type { ConsoleLevel, HandlerDataConsole } from '../types-hoist/instrument'; import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/debug-logger'; import { fill } from '../utils/object'; +import { stringMatchesSomePattern } from '../utils/string'; import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; +import { debug } from '../utils/debug-logger'; + +/** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + * This is a set to avoid duplicate integration setups to add the same filter multiple times. + */ +const _filter = new Set([]); /** * Add an instrumentation handler for when a console.xxx method is called. @@ -20,6 +30,27 @@ export function addConsoleInstrumentationHandler(handler: (data: HandlerDataCons return removeHandler; } +/** + * Add a filter to the console instrumentation to filter out console messages that match the given strings or regular expressions. + * Returns a function to remove the filter. + */ +export function addConsoleInstrumentationFilter(filter: (string | RegExp)[]): () => void { + for (const f of filter) { + _filter.add(f); + } + + return () => { + for (const f of filter) { + _filter.delete(f); + } + }; +} + +/** Only exported for tests. */ +export function _INTERNAL_resetConsoleInstrumentationOptions(): void { + _filter.clear(); +} + function instrumentConsole(): void { if (!('console' in GLOBAL_OBJ)) { return; @@ -34,10 +65,21 @@ function instrumentConsole(): void { originalConsoleMethods[level] = originalConsoleMethod; return function (...args: any[]): void { - triggerHandlers('console', { args, level } as HandlerDataConsole); - + const firstArg = args[0]; const log = originalConsoleMethods[level]; - log?.apply(GLOBAL_OBJ.console, args); + + const isFiltered = _filter.size && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, _filter); + + // Only trigger handlers for non-filtered messages + if (!isFiltered) { + triggerHandlers('console', { args, level } as HandlerDataConsole); + } + + // Only log filtered messages in debug mode + if (!isFiltered || (DEBUG_BUILD && debug.isEnabled())) { + // Call original console method + log?.apply(GLOBAL_OBJ.console, args); + } }; }); }); diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index e39fd5ddcf0d..21e1ee53d328 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -1,6 +1,6 @@ import { addBreadcrumb } from '../breadcrumbs'; import { getClient } from '../currentScopes'; -import { addConsoleInstrumentationHandler } from '../instrument/console'; +import { addConsoleInstrumentationFilter, addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { ConsoleLevel } from '../types-hoist/instrument'; import { CONSOLE_LEVELS } from '../utils/debug-logger'; @@ -10,6 +10,11 @@ import { GLOBAL_OBJ } from '../utils/worldwide'; interface ConsoleIntegrationOptions { levels: ConsoleLevel[]; + /** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + */ + filter?: (string | RegExp)[]; } type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { @@ -48,8 +53,12 @@ export const consoleIntegration = defineIntegration((options: Partial boolean)> = [], + patterns: + | Array boolean)> + | Set boolean)> = [], requireExactStringMatch: boolean = false, ): boolean { - return patterns.some(pattern => isMatchingPattern(testString, pattern, requireExactStringMatch)); + for (const pattern of patterns) { + if (isMatchingPattern(testString, pattern, requireExactStringMatch)) { + return true; + } + } + return false; } diff --git a/packages/core/test/lib/instrument/console.test.ts b/packages/core/test/lib/instrument/console.test.ts index 2499a231712d..f67f7da1a1a6 100644 --- a/packages/core/test/lib/instrument/console.test.ts +++ b/packages/core/test/lib/instrument/console.test.ts @@ -1,22 +1,110 @@ -import { describe, expect, it, vi } from 'vitest'; -import { addConsoleInstrumentationHandler } from '../../../src/instrument/console'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + _INTERNAL_resetConsoleInstrumentationOptions, + addConsoleInstrumentationFilter, + addConsoleInstrumentationHandler, +} from '../../../src/instrument/console'; import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; +import { debug, originalConsoleMethods } from '../../../src/utils/debug-logger'; +import { resetInstrumentationHandlers } from '../../../src/instrument/handlers'; describe('addConsoleInstrumentationHandler', () => { + let _originalConsoleMethods: typeof originalConsoleMethods = {}; + + afterEach(() => { + Object.assign(originalConsoleMethods, _originalConsoleMethods); + resetInstrumentationHandlers(); + vi.restoreAllMocks(); + }); + + // This cannot be done in beforeEach, as the first invocation of `addConsoleInstrumentationHandler` will overwrite the original console methods. + // Due to `fill` being called + // So instead, we need to call this each time after calling `addConsoleInstrumentationHandler` + function mockConsoleMethods() { + // Re-store this with the current implementation + Object.assign(_originalConsoleMethods, originalConsoleMethods); + + // Overwrite with mock console methods + Object.assign(originalConsoleMethods, { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + }); + } + it.each(['log', 'warn', 'error', 'debug', 'info'] as const)( 'calls registered handler when console.%s is called', level => { const handler = vi.fn(); addConsoleInstrumentationHandler(handler); + mockConsoleMethods(); GLOBAL_OBJ.console[level]('test message'); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['test message'], level })); + expect(originalConsoleMethods[level]).toHaveBeenCalledWith('test message'); }, ); it('calls through to the underlying console method without throwing', () => { addConsoleInstrumentationHandler(vi.fn()); + mockConsoleMethods(); expect(() => GLOBAL_OBJ.console.log('hello')).not.toThrow(); }); + + describe('filter', () => { + afterEach(() => { + _INTERNAL_resetConsoleInstrumentationOptions(); + }); + + describe('when debug is disabled', () => { + beforeEach(() => { + vi.spyOn(debug, 'isEnabled').mockImplementation(() => false); + }); + + it('filters out messages that match the filter', () => { + const handler = vi.fn(); + addConsoleInstrumentationHandler(handler); + addConsoleInstrumentationFilter(['test message']); + mockConsoleMethods(); + + GLOBAL_OBJ.console.log('test message'); + + expect(originalConsoleMethods.log).not.toHaveBeenCalledWith('test message'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('does not filter out messages that do not match the filter', () => { + const handler = vi.fn(); + addConsoleInstrumentationHandler(handler); + addConsoleInstrumentationFilter(['test message']); + mockConsoleMethods(); + + GLOBAL_OBJ.console.log('other message'); + + expect(handler).toHaveBeenCalled(); + expect(originalConsoleMethods.log).toHaveBeenCalledWith('other message'); + }); + }); + + describe('when debug is enabled', () => { + beforeEach(() => { + vi.spyOn(debug, 'isEnabled').mockImplementation(() => true); + }); + + it('logs filtered messages but does not call the handler for them', () => { + const handler = vi.fn(); + addConsoleInstrumentationHandler(handler); + addConsoleInstrumentationFilter(['test message']); + mockConsoleMethods(); + + GLOBAL_OBJ.console.log('test message'); + + expect(handler).not.toHaveBeenCalled(); + expect(originalConsoleMethods.log).toHaveBeenCalledWith('test message'); + }); + }); + }); }); diff --git a/packages/node-core/src/integrations/console.ts b/packages/node-core/src/integrations/console.ts index d958e00bdf12..d85e00e6b9d7 100644 --- a/packages/node-core/src/integrations/console.ts +++ b/packages/node-core/src/integrations/console.ts @@ -14,6 +14,11 @@ import { interface ConsoleIntegrationOptions { levels: ConsoleLevel[]; + /** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + */ + filter?: (string | RegExp)[]; } /** @@ -40,6 +45,10 @@ export const consoleIntegration = defineIntegration((options: Partial Date: Mon, 11 May 2026 13:23:16 +0200 Subject: [PATCH 13/32] feat(core): Add `applicationKey` to `BuildTimeOptionsBase` (#20789) Adds a top-level `applicationKey` option to the shared `BuildTimeOptionsBase` interface, so users can configure `thirdPartyErrorFilterIntegration` without needing `unstable_*` escape hatches. Wires up forwarding in: - React Router - SvelteKit - Astro - Nuxt Will do Next.js in a follow up as this does not implement the interface closes https://github.com/getsentry/sentry-javascript/issues/17384 --- packages/astro/src/integration/index.ts | 2 ++ packages/astro/test/integration/index.test.ts | 15 +++++++++++++++ .../build-time-plugins/buildTimeOptionsBase.ts | 9 +++++++++ packages/nuxt/src/vite/sourceMaps.ts | 1 + packages/nuxt/test/vite/sourceMaps.test.ts | 12 ++++++++++++ .../src/vite/makeCustomSentryVitePlugins.ts | 2 ++ .../test/vite/makeCustomSentryVitePlugins.test.ts | 12 ++++++++++++ .../test/vite/sentrySvelteKitPlugins.test.ts | 14 ++++++++++++++ .../tanstackstart-react/src/vite/sourceMaps.ts | 2 ++ .../test/vite/sourceMaps.test.ts | 12 ++++++++++++ 10 files changed, 81 insertions(+) diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 5c5ca2710af6..a1b4df28b003 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -35,6 +35,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // todo(v11): Extract `release` build time option here - cannot be done currently, because it conflicts with the `DeprecatedRuntimeOptions` type // release, bundleSizeOptimizations, + applicationKey, unstable_sentryVitePluginOptions, debug, org, @@ -109,6 +110,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { }, plugins: [ sentryVitePlugin({ + applicationKey, // Priority: top-level options > deprecated options > env vars // eslint-disable-next-line deprecation/deprecation org: org ?? uploadOptions.org ?? env.SENTRY_ORG, diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts index 15b04ac041bc..a7b4d68f16c3 100644 --- a/packages/astro/test/integration/index.test.ts +++ b/packages/astro/test/integration/index.test.ts @@ -269,6 +269,21 @@ describe('sentryAstro integration', () => { ); }); + it('passes top-level applicationKey to the vite plugin', async () => { + const integration = sentryAstro({ + applicationKey: 'my-app-key', + sourceMapsUploadOptions: { enabled: true, org: 'my-org', project: 'my-project' }, + }); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config }); + + expect(sentryVitePluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ + applicationKey: 'my-app-key', + }), + ); + }); + it("doesn't enable source maps if `sourceMapsUploadOptions.enabled` is `false`", async () => { const integration = sentryAstro({ sourceMapsUploadOptions: { enabled: false }, diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index f61aa6c40c94..c87ce1198df4 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -125,6 +125,15 @@ export interface BuildTimeOptionsBase { * Options for bundle size optimizations by excluding certain features of the Sentry SDK. */ bundleSizeOptimizations?: BundleSizeOptimizationsOptions; + + /** + * A key that is used to identify the application in the Sentry bundler plugins. + * This key is used by the `thirdPartyErrorFilterIntegration` to filter out errors + * originating from third-party scripts. + * + * @see https://docs.sentry.io/platforms/javascript/configuration/filtering/#using-thirdpartyerrorfilterintegration + */ + applicationKey?: string; } /** diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 16d0fd330649..333a22e4e189 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -171,6 +171,7 @@ export function getPluginOptions( } return { + applicationKey: moduleOptions.applicationKey, // eslint-disable-next-line deprecation/deprecation org: moduleOptions.org ?? sourceMapsUploadOptions.org ?? process.env.SENTRY_ORG, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts index 28e0336f43f5..ea80f32efd89 100644 --- a/packages/nuxt/test/vite/sourceMaps.test.ts +++ b/packages/nuxt/test/vite/sourceMaps.test.ts @@ -231,6 +231,18 @@ describe('getPluginOptions', () => { }); }); + it('passes applicationKey to plugin options', () => { + const options: SentryNuxtModuleOptions = { + applicationKey: 'my-app-key', + }; + + const result = getPluginOptions(options); + + expect(result).toMatchObject({ + applicationKey: 'my-app-key', + }); + }); + it('supports bundleSizeOptimizations', () => { const options: SentryNuxtModuleOptions = { bundleSizeOptimizations: { diff --git a/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts b/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts index fcec109c6baa..69b07e1da28f 100644 --- a/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts +++ b/packages/react-router/src/vite/makeCustomSentryVitePlugins.ts @@ -10,6 +10,7 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil debug, unstable_sentryVitePluginOptions, bundleSizeOptimizations, + applicationKey, authToken, org, project, @@ -19,6 +20,7 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil } = options; const sentryVitePlugins = sentryVitePlugin({ + applicationKey, authToken: authToken ?? process.env.SENTRY_AUTH_TOKEN, bundleSizeOptimizations, debug: debug ?? false, diff --git a/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts b/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts index b98a6ebfb80d..c38e80ef72df 100644 --- a/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts +++ b/packages/react-router/test/vite/makeCustomSentryVitePlugins.test.ts @@ -48,6 +48,18 @@ describe('makeCustomSentryVitePlugins', () => { ); }); + it('should pass applicationKey to sentryVitePlugin', async () => { + await makeCustomSentryVitePlugins({ + applicationKey: 'my-app-key', + }); + + expect(sentryVitePlugin).toHaveBeenCalledWith( + expect.objectContaining({ + applicationKey: 'my-app-key', + }), + ); + }); + it('should return all plugins from sentryVitePlugin', async () => { const plugins = await makeCustomSentryVitePlugins({}); expect(plugins).toHaveLength(1); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 21e5970680af..7956c078e9f6 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -207,6 +207,20 @@ describe('generateVitePluginOptions', () => { expect(result).toBeNull(); }); + it('passes applicationKey through to vite plugin options', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + const options: SentrySvelteKitPluginOptions = { + autoUploadSourceMaps: true, + applicationKey: 'my-app-key', + }; + const result = generateVitePluginOptions(options); + expect(result).toEqual(expect.objectContaining({ applicationKey: 'my-app-key' })); + + process.env.NODE_ENV = originalEnv; + }); + it('uses default `debug` value if only default options are provided', () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; // Ensure we're not in development mode diff --git a/packages/tanstackstart-react/src/vite/sourceMaps.ts b/packages/tanstackstart-react/src/vite/sourceMaps.ts index 296e8582cde8..38c4e8750bd6 100644 --- a/packages/tanstackstart-react/src/vite/sourceMaps.ts +++ b/packages/tanstackstart-react/src/vite/sourceMaps.ts @@ -9,6 +9,7 @@ type FilesToDeleteAfterUpload = string | string[] | undefined; */ export function makeAddSentryVitePlugin(options: BuildTimeOptionsBase): Plugin[] { const { + applicationKey, authToken, bundleSizeOptimizations, debug, @@ -52,6 +53,7 @@ export function makeAddSentryVitePlugin(options: BuildTimeOptionsBase): Plugin[] }; const sentryPlugins = sentryVitePlugin({ + applicationKey, authToken: authToken ?? process.env.SENTRY_AUTH_TOKEN, bundleSizeOptimizations: bundleSizeOptimizations ?? undefined, debug: debug ?? false, diff --git a/packages/tanstackstart-react/test/vite/sourceMaps.test.ts b/packages/tanstackstart-react/test/vite/sourceMaps.test.ts index 58567f085b72..672a94892a58 100644 --- a/packages/tanstackstart-react/test/vite/sourceMaps.test.ts +++ b/packages/tanstackstart-react/test/vite/sourceMaps.test.ts @@ -98,6 +98,18 @@ describe('makeAddSentryVitePlugin()', () => { ); }); + it('passes applicationKey to sentryVitePlugin', () => { + makeAddSentryVitePlugin({ + applicationKey: 'my-app-key', + }); + + expect(sentryVitePluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ + applicationKey: 'my-app-key', + }), + ); + }); + it('returns Sentry Vite plugins and config plugin', () => { const plugins = makeAddSentryVitePlugin({ org: 'my-org', From aac924e2bd2c231ac4676b6f57d7212815f3e8d7 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 13:41:59 +0200 Subject: [PATCH 14/32] fix(node-core): Guard against undefined util.getSystemErrorMap (#20660) The `systemErrorIntegration` in `@sentry/node-core` attempts to call `util.getSystemErrorMap()` to determine if an error is a Node.js SystemError. However, in environments like Bun (which provides a Node.js compatibility layer), `util.getSystemErrorMap` may be undefined. This leads to a `TypeError: util.getSystemErrorMap is not a function` when Sentry tries to process any event, effectively crashing the Sentry event pipeline. This change adds a `typeof` check for `util.getSystemErrorMap` within the `isSystemError` function. If the function is not available, `isSystemError` will now gracefully return `false`, preventing the `TypeError` and allowing Sentry's event processing to continue. This means that system error context enrichment will be skipped in environments where this specific `util` API is missing, which is the desired fallback behavior. See also: https://github.com/oven-sh/bun/issues/22872 Fixes XCODEBUILDMCP-1J --------- Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> Co-authored-by: Hector Co-authored-by: JPeer264 --- .../node-core/src/integrations/systemError.ts | 7 + .../test/integrations/systemError.test.ts | 141 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 packages/node-core/test/integrations/systemError.test.ts diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts index 14c0c23ffa54..29c4410a01f9 100644 --- a/packages/node-core/src/integrations/systemError.ts +++ b/packages/node-core/src/integrations/systemError.ts @@ -20,6 +20,13 @@ function isSystemError(error: unknown): error is SystemError { return false; } + // Workaround for Bun where getSystemErrorMap doesn't exist + // Can be removed once Bun supports getSystemErrorMap + // https://github.com/oven-sh/bun/issues/22872 + if (typeof util.getSystemErrorMap !== 'function') { + return false; + } + // Appears this is the recommended way to check for Node.js SystemError // https://github.com/nodejs/node/issues/46869 return util.getSystemErrorMap().has(error.errno); diff --git a/packages/node-core/test/integrations/systemError.test.ts b/packages/node-core/test/integrations/systemError.test.ts new file mode 100644 index 000000000000..3d29be490104 --- /dev/null +++ b/packages/node-core/test/integrations/systemError.test.ts @@ -0,0 +1,141 @@ +import type { Client, Event } from '@sentry/core'; +import type * as nodeUtil from 'node:util'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { systemErrorIntegration } from '../../src/integrations/systemError'; + +const mocks = vi.hoisted(() => ({ + getSystemErrorMap: vi.fn() as ReturnType | undefined, +})); + +vi.mock('node:util', async importOriginal => { + const actual = (await importOriginal()) as typeof nodeUtil; + mocks.getSystemErrorMap = vi.fn(actual.getSystemErrorMap); + return { + ...actual, + get getSystemErrorMap() { + return mocks.getSystemErrorMap; + }, + }; +}); + +import * as util from 'node:util'; + +describe('systemErrorIntegration', () => { + afterEach(() => { + vi.mocked(util.getSystemErrorMap).mockRestore(); + }); + + function createClient(sendDefaultPii = false): Client { + return { + getOptions: () => ({ sendDefaultPii }), + } as unknown as Client; + } + + it('returns the event unchanged when util.getSystemErrorMap is undefined (e.g. Bun)', () => { + const originalFn = mocks.getSystemErrorMap; + mocks.getSystemErrorMap = undefined; + + try { + const integration = systemErrorIntegration(); + const error = Object.assign(new Error('boom'), { errno: -2, path: '/some/path' }); + const event = { exception: { values: [{ value: error.message }] } } as Event; + + const result = integration.processEvent!(event, { originalException: error }, createClient()) as Event; + + expect(result).toBe(event); + expect(result.contexts?.node_system_error).toBeUndefined(); + } finally { + mocks.getSystemErrorMap = originalFn; + } + }); + + it('adds node_system_error context for a real SystemError', () => { + const errno = -2; + vi.mocked(util.getSystemErrorMap).mockReturnValue( + new Map([[errno, ['ENOENT', 'no such file or directory']]]), + ); + + const integration = systemErrorIntegration(); + const error = Object.assign(new Error("ENOENT: no such file or directory, open '/secret/path'"), { + errno, + path: '/secret/path', + }); + const event = { exception: { values: [{ value: error.message }] } } as Event; + + const result = integration.processEvent!(event, { originalException: error }, createClient()) as Event; + + expect(result.contexts?.node_system_error).toEqual({ errno }); + expect(result.exception?.values?.[0]?.value).not.toContain('/secret/path'); + }); + + it('keeps path in context when sendDefaultPii is true', () => { + const errno = -2; + vi.mocked(util.getSystemErrorMap).mockReturnValue( + new Map([[errno, ['ENOENT', 'no such file or directory']]]), + ); + + const integration = systemErrorIntegration(); + const error = Object.assign(new Error('boom'), { errno, path: '/secret/path' }); + const event = { exception: { values: [{ value: error.message }] } } as Event; + + const result = integration.processEvent!(event, { originalException: error }, createClient(true)) as Event; + + expect(result.contexts?.node_system_error).toEqual({ errno, path: '/secret/path' }); + }); + + it('keeps path in context when includePaths option is true', () => { + const errno = -2; + vi.mocked(util.getSystemErrorMap).mockReturnValue( + new Map([[errno, ['ENOENT', 'no such file or directory']]]), + ); + + const integration = systemErrorIntegration({ includePaths: true }); + const error = Object.assign(new Error('boom'), { errno, path: '/secret/path' }); + const event = { exception: { values: [{ value: error.message }] } } as Event; + + const result = integration.processEvent!(event, { originalException: error }, createClient()) as Event; + + expect(result.contexts?.node_system_error).toEqual({ errno, path: '/secret/path' }); + }); + + it('returns the event unchanged when the error has no errno', () => { + vi.mocked(util.getSystemErrorMap).mockReturnValue( + new Map([[-2, ['ENOENT', 'no such file or directory']]]), + ); + + const integration = systemErrorIntegration(); + const error = new Error('not a system error'); + const event = {} as Event; + + const result = integration.processEvent!(event, { originalException: error }, createClient()) as Event; + + expect(result?.contexts?.node_system_error).toBeUndefined(); + }); + + it('returns the event unchanged when originalException is not an Error', () => { + vi.mocked(util.getSystemErrorMap).mockReturnValue( + new Map([[-2, ['ENOENT', 'no such file or directory']]]), + ); + + const integration = systemErrorIntegration(); + const event = {} as Event; + + const result = integration.processEvent!(event, { originalException: 'not an error' }, createClient()) as Event; + + expect(result.contexts?.node_system_error).toBeUndefined(); + }); + + it('returns the event unchanged when errno is not in the system error map', () => { + vi.mocked(util.getSystemErrorMap).mockReturnValue( + new Map([[-2, ['ENOENT', 'no such file or directory']]]), + ); + + const integration = systemErrorIntegration(); + const error = Object.assign(new Error('unknown'), { errno: 99999 }); + const event = {} as Event; + + const result = integration.processEvent!(event, { originalException: error }, createClient()) as Event; + + expect(result.contexts?.node_system_error).toBeUndefined(); + }); +}); From afe03a3d278872bf3a3d53d913a5fda382b71230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Mon, 11 May 2026 14:55:59 +0200 Subject: [PATCH 15/32] chore: Typo intergation -> integration (#20799) There are also 3 typos in the changelog, not sure if we should change them too though --- packages/core/test/lib/integration.test.ts | 2 +- .../src/integrations/http/httpServerSpansIntegration.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 75e13374daa7..e5362d669716 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -73,7 +73,7 @@ describe('getIntegrationsToSetup', () => { expect(integrations.map(i => i.name)).toEqual(expected); }); - test('it uses passed integration over default intergation', () => { + test('it uses passed integration over default integration', () => { const integrationDefault = new MockIntegration('ChaseSquirrels'); const integration1 = new MockIntegration('ChaseSquirrels'); diff --git a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts index 3d70387df415..314f733a7dae 100644 --- a/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts +++ b/packages/node-core/src/integrations/http/httpServerSpansIntegration.ts @@ -249,13 +249,13 @@ const _httpServerSpansIntegration = ((options: HttpServerSpansIntegrationOptions if (client.getIntegrationByName('Http')) { debug.warn( - 'It seems that you have manually added `httpServerSpansIntergation` while `httpIntegration` is also present. Make sure to remove `httpIntegration` when adding `httpServerSpansIntegration`.', + 'It seems that you have manually added `httpServerSpansIntegration` while `httpIntegration` is also present. Make sure to remove `httpIntegration` when adding `httpServerSpansIntegration`.', ); } if (!client.getIntegrationByName('Http.Server')) { debug.error( - 'It seems that you have manually added `httpServerSpansIntergation` without adding `httpServerIntegration`. This is a requiement for spans to be created - please add the `httpServerIntegration` integration.', + 'It seems that you have manually added `httpServerSpansIntegration` without adding `httpServerIntegration`. This is a requiement for spans to be created - please add the `httpServerIntegration` integration.', ); } }, From b6cb7b6d858c87cec0fadf3bd912747ddd8cfded Mon Sep 17 00:00:00 2001 From: Sigrid <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 11 May 2026 14:57:50 +0200 Subject: [PATCH 16/32] fix(hono): Capture transaction name on request for correct culprit (#20801) Hono error events were showing a wrong culprit (`?(index)`) because the transaction name was only being set in the response handler. When an error was thrown during middleware execution, `captureException` ran before `responseHandler` had a chance to update the span name and isolation scope transaction name. This PR moves the span and transaction name update into `requestHandler` so the correct route name is set before any middleware or route handler executes. The `responseHandler` now only captures errors. Also switches from `routePath(context)` (last matched route so far) to `routePath(context, -1)` ([final matched route](https://hono.dev/docs/helpers/route#using-with-index-parameter)) which avoids wildcard patterns like `/error/*` leaking into the transaction name. Also adds another test for route parametrization when using middleware. Closes https://github.com/getsentry/sentry-javascript/issues/20399 --- .../src/route-groups/test-middleware.ts | 2 ++ .../test-applications/hono-4/src/routes.ts | 1 + .../hono-4/tests/errors.test.ts | 3 ++- .../hono-4/tests/middleware.test.ts | 24 +++++++++++++++++- .../hono/src/shared/middlewareHandlers.ts | 25 ++++++++++++------- .../test/shared/middlewareHandlers.test.ts | 6 +++-- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts index 49ca50c591bf..d82201b7cdb3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts @@ -7,6 +7,7 @@ middlewareRoutes.get('/named', c => c.json({ middleware: 'named' })); middlewareRoutes.get('/anonymous', c => c.json({ middleware: 'anonymous' })); middlewareRoutes.get('/multi', c => c.json({ middleware: 'multi' })); middlewareRoutes.get('/error', c => c.text('should not reach')); +middlewareRoutes.get('/param/:id', c => c.json({ paramId: c.req.param('id') })); // Self-contained sub-app registering its own middleware via .use() const subAppWithMiddleware = new Hono(); @@ -18,6 +19,7 @@ subAppWithMiddleware.use('/anonymous/*', async (c, next) => { }); subAppWithMiddleware.use('/multi/*', middlewareA, middlewareB); subAppWithMiddleware.use('/error/*', failingMiddleware); +subAppWithMiddleware.use('/param/*', middlewareA); // .all() handler (1 parameter) — should NOT be wrapped as middleware by patchRoute. subAppWithMiddleware.all('/all-handler', async function allCatchAll(c) { diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts index cfb13146b6f7..f1f9c3a783b3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts @@ -34,6 +34,7 @@ export function addRoutes(app: Hono<{ Bindings?: { E2E_TEST_DSN: string } }>): v }); app.use('/test-middleware/multi/*', middlewareA, middlewareB); app.use('/test-middleware/error/*', failingMiddleware); + app.use('/test-middleware/param/*', middlewareA); app.route('/test-middleware', middlewareRoutes); // Sub-app middleware: registered on the sub-app, wrapped at mount time by route() patching diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts index 98c81d30afeb..8ee1f13f0c0a 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts @@ -147,6 +147,7 @@ test.describe('middleware errors', () => { expect(errorEvent.exception?.values?.[0]?.value).toBe('Service Unavailable from middleware'); expect(errorEvent.exception?.values?.[0]?.mechanism?.type).toBe('auto.middleware.hono'); expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false); + expect(errorEvent.transaction).toBe('GET /test-errors/middleware-http-exception'); const transaction = await transactionPromise; const middlewareSpan = (transaction.spans || []).find(s => s.op === 'middleware.hono'); @@ -183,7 +184,7 @@ test.describe('middleware errors', () => { const transaction = await transactionPromise; if (RUNTIME === 'cloudflare') { - expect(transaction.transaction).toBe('GET /test-errors/middleware-http-exception-4xx/*'); + expect(transaction.transaction).toBe('GET /test-errors/middleware-http-exception-4xx'); const middlewareSpan = (transaction.spans || []).find(s => s.op === 'middleware.hono'); expect(middlewareSpan?.status).not.toBe('internal_error'); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts index d984ac0d38a8..c4bf34874f2b 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts @@ -116,6 +116,9 @@ for (const { name, prefix } of SCENARIOS) { type: 'auto.middleware.hono', }), ); + + // The transaction name on the error event determines the culprit shown in Sentry. + expect(errorEvent.transaction).toBe(`GET ${prefix}/error`); }); test('sets error status on middleware span when middleware throws', async ({ baseURL }) => { @@ -126,7 +129,7 @@ for (const { name, prefix } of SCENARIOS) { await fetch(`${baseURL}${prefix}/error`); const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/error/*`); + expect(transaction.transaction).toBe(`GET ${prefix}/error`); const spans = transaction.spans || []; @@ -138,6 +141,25 @@ for (const { name, prefix } of SCENARIOS) { expect(failingSpan?.status).toBe('internal_error'); }); + test('uses parameterized route in transaction name', async ({ baseURL }) => { + const transactionPromise = waitForTransaction(APP_NAME, event => { + return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/param/`); + }); + + const response = await fetch(`${baseURL}${prefix}/param/42`); + expect(response.status).toBe(200); + + const transaction = await transactionPromise; + expect(transaction.transaction).toBe(`GET ${prefix}/param/:id`); + + const spans = transaction.spans || []; + const middlewareSpan = spans.find( + (span: { description?: string; op?: string }) => + span.op === 'middleware.hono' && span.description === 'middlewareA', + ); + expect(middlewareSpan).toBeDefined(); + }); + test('includes request data on error events from middleware', async ({ baseURL }) => { const errorPromise = waitForError(APP_NAME, event => { return event.exception?.values?.[0]?.value === 'Middleware error' && !!event.request?.url?.includes(prefix); diff --git a/packages/hono/src/shared/middlewareHandlers.ts b/packages/hono/src/shared/middlewareHandlers.ts index 41902d90f84f..03bb6e16da58 100644 --- a/packages/hono/src/shared/middlewareHandlers.ts +++ b/packages/hono/src/shared/middlewareHandlers.ts @@ -6,6 +6,7 @@ import { getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, updateSpanName, + type Scope, winterCGRequestToRequestData, } from '@sentry/core'; import type { Context } from 'hono'; @@ -22,6 +23,8 @@ export function requestHandler(context: Context): void { const isolationScope = defaultScope === currentIsolationScope ? defaultScope : currentIsolationScope; + updateSpanRouteName(isolationScope, context); + isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(hasFetchEvent(context) ? context.event.request : context.req.raw), }); @@ -31,21 +34,25 @@ export function requestHandler(context: Context): void { * Response handler for Hono framework */ export function responseHandler(context: Context): void { + if (context.error && !isExpectedError(context.error)) { + getClient()?.captureException(context.error, { + mechanism: { handled: false, type: 'auto.http.hono.context_error' }, + }); + } +} + +function updateSpanRouteName(isolationScope: Scope, context: Context): void { const activeSpan = getActiveSpan(); + const lastMatchedRoute = routePath(context, -1); + if (activeSpan) { - activeSpan.updateName(`${context.req.method} ${routePath(context)}`); + activeSpan.updateName(`${context.req.method} ${lastMatchedRoute}`); activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); const rootSpan = getRootSpan(activeSpan); - updateSpanName(rootSpan, `${context.req.method} ${routePath(context)}`); + updateSpanName(rootSpan, `${context.req.method} ${lastMatchedRoute}`); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } - getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`); - - if (context.error && !isExpectedError(context.error)) { - getClient()?.captureException(context.error, { - mechanism: { handled: false, type: 'auto.http.hono.context_error' }, - }); - } + isolationScope.setTransactionName(`${context.req.method} ${lastMatchedRoute}`); } diff --git a/packages/hono/test/shared/middlewareHandlers.test.ts b/packages/hono/test/shared/middlewareHandlers.test.ts index b8e4cdef1062..accf5fe5f91a 100644 --- a/packages/hono/test/shared/middlewareHandlers.test.ts +++ b/packages/hono/test/shared/middlewareHandlers.test.ts @@ -1,6 +1,6 @@ import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { responseHandler } from '../../src/shared/middlewareHandlers'; +import { requestHandler, responseHandler } from '../../src/shared/middlewareHandlers'; vi.mock('hono/route', () => ({ routePath: () => '/test', @@ -11,6 +11,7 @@ vi.mock('../../src/utils/hono-context', () => ({ })); const mockSetTransactionName = vi.fn(); +const mockSetSDKProcessingMetadata = vi.fn(); vi.mock('@sentry/core', async () => { const actual = await vi.importActual('@sentry/core'); @@ -19,6 +20,7 @@ vi.mock('@sentry/core', async () => { getActiveSpan: vi.fn(() => null), getIsolationScope: vi.fn(() => ({ setTransactionName: mockSetTransactionName, + setSDKProcessingMetadata: mockSetSDKProcessingMetadata, })), getClient: vi.fn(() => undefined), }; @@ -110,7 +112,7 @@ describe('responseHandler', () => { describe('transaction name', () => { it('sets transaction name on isolation scope', () => { // oxlint-disable-next-line typescript/no-explicit-any - responseHandler(createMockContext(200) as any); + requestHandler(createMockContext(200) as any); expect(mockSetTransactionName).toHaveBeenCalledWith('GET /test'); }); From ca83ba9232142cb264c8423b4b57b652a70cb0aa Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 11 May 2026 14:59:45 +0200 Subject: [PATCH 17/32] feat(nextjs): Add top-level `applicationKey` option (#20794) Adds a top-level `applicationKey` option to SentryBuildOptions so users can configure `thirdPartyErrorFilterIntegration` without needing experimental or unstable apis. Forwards to both webpack and turbopack. ref https://github.com/getsentry/sentry-javascript/issues/17384 --- .../src/config/getBuildPluginOptions.ts | 1 + .../turbopack/constructTurbopackConfig.ts | 3 +- packages/nextjs/src/config/types.ts | 16 ++++++++- .../test/config/getBuildPluginOptions.test.ts | 15 +++++++++ .../constructTurbopackConfig.test.ts | 33 +++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 5018e1b4b196..e616c49263af 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -309,6 +309,7 @@ export function getBuildPluginOptions({ const skipSourcemapsUpload = shouldSkipSourcemapUpload(buildTool, useRunAfterProductionCompileHook); return { + applicationKey: sentryBuildOptions.applicationKey, authToken: sentryBuildOptions.authToken, headers: sentryBuildOptions.headers, org: sentryBuildOptions.org, diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 31530f07c042..bf919e644754 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -66,7 +66,8 @@ export function constructTurbopackConfig({ // so it is safe even for node_modules with strict initialization order. // We only exclude Next.js build polyfills which contain non-standard syntax that causes // parse errors when any code is prepended (Turbopack re-parses the loader output). - const applicationKey = userSentryOptions?._experimental?.turbopackApplicationKey; + // eslint-disable-next-line deprecation/deprecation + const applicationKey = userSentryOptions?.applicationKey ?? userSentryOptions?._experimental?.turbopackApplicationKey; if (applicationKey && nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion)) { newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher: '*.{ts,tsx,js,jsx,mjs,cjs}', diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 86068841e773..722c3f58f570 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -463,6 +463,18 @@ export type SentryBuildOptions = { }; }; + /** + * A key that is used to identify the application in the Sentry bundler plugins. + * This key is used by the `thirdPartyErrorFilterIntegration` to filter out errors + * originating from third-party scripts. + * + * For webpack builds, this is forwarded to the `@sentry/webpack-plugin`. + * For Turbopack builds, this injects module metadata via a custom loader. + * + * @see https://docs.sentry.io/platforms/javascript/configuration/filtering/#using-thirdpartyerrorfilterintegration + */ + applicationKey?: string; + /** * Options to configure various bundle size optimizations related to the Sentry SDK. */ @@ -738,8 +750,10 @@ export type SentryBuildOptions = { * webpack builds via its `moduleMetadata` / `applicationKey` option. * * Requires Next.js 16+ + * + * @deprecated Use the top-level `applicationKey` option instead, which works for both webpack and Turbopack builds. */ - turbopackApplicationKey?: string; + turbopackApplicationKey?: string; // TODO(v11): remove this option /** * Options for React component name annotation in Turbopack builds. * When enabled, JSX elements are annotated with `data-sentry-component`, diff --git a/packages/nextjs/test/config/getBuildPluginOptions.test.ts b/packages/nextjs/test/config/getBuildPluginOptions.test.ts index c67135a5d8d3..0b8a729e5942 100644 --- a/packages/nextjs/test/config/getBuildPluginOptions.test.ts +++ b/packages/nextjs/test/config/getBuildPluginOptions.test.ts @@ -61,6 +61,21 @@ describe('getBuildPluginOptions', () => { }); }); + it('forwards applicationKey to plugin options', () => { + const sentryBuildOptions: SentryBuildOptions = { + applicationKey: 'my-app-key', + }; + + const result = getBuildPluginOptions({ + sentryBuildOptions, + releaseName: mockReleaseName, + distDirAbsPath: mockDistDirAbsPath, + buildTool: 'after-production-compile-webpack', + }); + + expect(result.applicationKey).toBe('my-app-key'); + }); + it('normalizes Windows paths to posix for glob patterns in after-production-compile builds', () => { const windowsPath = 'C:\\Users\\test\\.next'; const sentryBuildOptions: SentryBuildOptions = { diff --git a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts index 663317a8bd69..242174f015de 100644 --- a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts +++ b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts @@ -977,6 +977,39 @@ describe('moduleMetadataInjection with applicationKey', () => { }); }); + it('should add metadata loader rule when top-level applicationKey is set and Next.js >= 16', () => { + const userNextConfig: NextConfigObject = {}; + + const result = constructTurbopackConfig({ + userNextConfig, + userSentryOptions: { applicationKey: 'my-top-level-key' }, + nextJsVersion: '16.0.0', + }); + + const rule = result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}'] as { + loaders: Array<{ loader: string; options: { applicationKey: string } }>; + }; + expect(rule.loaders[0]!.options.applicationKey).toBe('my-top-level-key'); + }); + + it('should prefer top-level applicationKey over deprecated _experimental.turbopackApplicationKey', () => { + const userNextConfig: NextConfigObject = {}; + + const result = constructTurbopackConfig({ + userNextConfig, + userSentryOptions: { + applicationKey: 'top-level-key', + _experimental: { turbopackApplicationKey: 'deprecated-key' }, + }, + nextJsVersion: '16.0.0', + }); + + const rule = result.rules!['*.{ts,tsx,js,jsx,mjs,cjs}'] as { + loaders: Array<{ loader: string; options: { applicationKey: string } }>; + }; + expect(rule.loaders[0]!.options.applicationKey).toBe('top-level-key'); + }); + it('should only exclude Next.js polyfills, not all foreign modules', () => { const userNextConfig: NextConfigObject = {}; From 43a4bff1d47bf40b6908b76fa8261c195276672d Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Mon, 11 May 2026 06:13:13 -0700 Subject: [PATCH 18/32] test(cloudflare-hono): fix 'occured' -> 'occurred' typo in error log (#20783) Fixes a spelling typo (`occured` -> `occurred`) in the cloudflare-hono e2e test app's error log message. --- .../e2e-tests/test-applications/cloudflare-hono/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts index 7cd667c72408..ceba3494d53f 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts @@ -17,7 +17,7 @@ app.get('/error', () => { }); app.onError((err, ctx) => { - console.error(`Error occured: ${err.message}`); + console.error(`Error occurred: ${err.message}`); return ctx.json({ error: err.message }, 500); }); From ed8602c63d1e7702ef23cd43a0958c7a71041cb2 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 16:01:01 +0200 Subject: [PATCH 19/32] feat(node): Support Node 26 (#20710) This adds v26 to the node test matrix on CI, ensuring we support this properly. --------- Co-authored-by: JPeer264 --- .github/workflows/build.yml | 8 +- .../utils/runner.ts | 3 +- .../integrations/console/filter/test.ts | 15 ++- .../suites/tracing/postgres/package.json | 4 +- .../suites/tracing/postgres/test.ts | 101 +++++++++--------- .../suites/tracing/postgres/yarn.lock | 56 +++++----- .../node-integration-tests/utils/runner.ts | 3 +- .../node-core/src/integrations/console.ts | 9 +- packages/node-native/package.json | 2 +- .../scripts/prune-profiler-binaries.js | 3 + packages/profiling-node/src/integration.ts | 2 +- yarn.lock | 8 +- 12 files changed, 118 insertions(+), 96 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index daaf5effc2a0..4f19d81a92e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -554,7 +554,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, 24] + node: [18, 20, 22, 24, 26] steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v6 @@ -810,7 +810,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, 24] + node: [18, 20, 22, 24, 26] typescript: - false include: @@ -851,7 +851,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, 24] + node: [18, 20, 22, 24, 26] typescript: - false include: @@ -939,7 +939,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, 24] + node: [18, 20, 22, 24, 26] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v6 diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts index d27c65fc81be..69163754c89e 100644 --- a/dev-packages/node-core-integration-tests/utils/runner.ts +++ b/dev-packages/node-core-integration-tests/utils/runner.ts @@ -486,7 +486,8 @@ export function createRunner(...paths: string[]) { if (process.env.DEBUG) log('stderr line', output); - if (ensureNoErrorOutput) { + // Ignore deprecation warnings for this purpose + if (ensureNoErrorOutput && !`${output}`.includes('DeprecationWarning:')) { complete(new Error(`Expected no error output but got: '${output}'`)); } }); diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts b/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts index 5f0bf9e8b14a..decbe91dbb00 100644 --- a/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts @@ -8,7 +8,7 @@ describe('Console Integration', () => { createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('filters console messages', async () => { - await createRunner() + const runner = createRunner() .expect({ event: { exception: { @@ -28,8 +28,17 @@ describe('Console Integration', () => { ], }, }) - .start() - .completed(); + .start(); + + await runner.completed(); + + expect(runner.getLogs()).toContainEqual('hello'); + expect(runner.getLogs()).toContainEqual('baz'); + expect(runner.getLogs()).not.toContainEqual('foo'); + expect(runner.getLogs()).not.toContainEqual('foo2'); + + // Ensure deprecation warnigns are not included + expect(runner.getLogs()).not.toContainEqual(expect.stringMatching('DeprecationWarning')); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/package.json b/dev-packages/node-integration-tests/suites/tracing/postgres/package.json index 602eab3a6f3d..040e636bcb69 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "pg": "8.16.0", - "pg-native": "3.5.0" + "pg": "8.20.0", + "pg-native": "3.7.0" } } diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index 98c42976498a..e5d0e3f26fa8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest'; import { createRunner } from '../../../utils/runner'; +import { conditionalTest } from '../../../utils'; describe('postgres auto instrumentation', () => { test('should auto-instrument `pg` package', { timeout: 90_000 }, async () => { @@ -49,7 +50,6 @@ describe('postgres auto instrumentation', () => { await createRunner(__dirname, 'scenario.js') .withDockerCompose({ workingDirectory: [__dirname], - setupCommand: 'yarn', }) .expect({ transaction: EXPECTED_TRANSACTION }) .start() @@ -60,7 +60,6 @@ describe('postgres auto instrumentation', () => { await createRunner(__dirname, 'scenario-ignoreConnect.js') .withDockerCompose({ workingDirectory: [__dirname], - setupCommand: 'yarn', }) .expect({ transaction: txn => { @@ -103,57 +102,59 @@ describe('postgres auto instrumentation', () => { .completed(); }); - test('should auto-instrument `pg-native` package', { timeout: 90_000 }, async () => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - 'db.system': 'postgresql', - 'db.name': 'tests', - 'sentry.origin': 'manual', - 'sentry.op': 'db', + conditionalTest({ max: 25 })('pg-native', () => { + test('should auto-instrument `pg-native` package', { timeout: 90_000 }, async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'sentry.origin': 'manual', + 'sentry.op': 'db', + }), + description: 'pg.connect', + op: 'db', + status: 'ok', }), - description: 'pg.connect', - op: 'db', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'db.system': 'postgresql', - 'db.name': 'tests', - 'db.statement': 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', - 'sentry.origin': 'auto.db.otel.postgres', - 'sentry.op': 'db', + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', }), - description: 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', - op: 'db', - status: 'ok', - origin: 'auto.db.otel.postgres', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'db.system': 'postgresql', - 'db.name': 'tests', - 'db.statement': 'SELECT * FROM "NativeUser"', - 'sentry.origin': 'auto.db.otel.postgres', - 'sentry.op': 'db', + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'SELECT * FROM "NativeUser"', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'SELECT * FROM "NativeUser"', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', }), - description: 'SELECT * FROM "NativeUser"', - op: 'db', - status: 'ok', - origin: 'auto.db.otel.postgres', - }), - ]), - }; + ]), + }; - await createRunner(__dirname, 'scenario-native.js') - .withDockerCompose({ - workingDirectory: [__dirname], - setupCommand: 'yarn', - }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start() - .completed(); + await createRunner(__dirname, 'scenario-native.js') + .withDockerCompose({ + workingDirectory: [__dirname], + setupCommand: 'yarn', + }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/postgres/yarn.lock index 9eb59e69f6c2..8b2e296ece0b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/yarn.lock +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/yarn.lock @@ -27,38 +27,38 @@ nan@~2.22.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== -pg-cloudflare@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz#2e3649c38a7a9c74a7e5327c8098a2fd9af595bd" - integrity sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg== +pg-cloudflare@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz#386035d4bfcf1a7045b026f8b21acf5353f14d65" + integrity sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ== -pg-connection-string@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.9.0.tgz#f75e06591fdd42ec7636fe2c6a03febeedbec9bf" - integrity sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ== +pg-connection-string@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.12.0.tgz#4084f917902bb2daae3dc1376fe24ac7b4eaccf2" + integrity sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ== pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-native@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-3.5.0.tgz#1a43c0d5f5744e40df3bf737c43178ce98984255" - integrity sha512-rj4LYouevTdKxvRLnvtOLEPOerkiPAqUdZE1K48IfQluEH/x7GrldEDdSaEOmJ6z7s6LQwDTpAPhm2s00iG8xw== +pg-native@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-3.7.0.tgz#1bd78031482c78dc5240c4350cddb291493dc34f" + integrity sha512-q2V5DynvPt4PD75q1DqZOUrieEgE4bf/flEeLCzzs8axgn8x2mRCUhd1DP0cqMz8FEdpVEDb0/zKblQWeijbGg== dependencies: libpq "^1.8.15" pg-types "2.2.0" -pg-pool@^3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.0.tgz#134b0213755c5e7135152976488aa7cd7ee1268d" - integrity sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA== +pg-pool@^3.13.0: + version "3.13.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.13.0.tgz#416482e9700e8f80c685a6ae5681697a413c13a3" + integrity sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA== -pg-protocol@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.10.0.tgz#a473afcbb1c6e5dc3ac24869ba3dd563f8a1ae1b" - integrity sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q== +pg-protocol@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.13.0.tgz#fdaf6d020bca590d58bb991b4b16fc448efe0511" + integrity sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w== pg-types@2.2.0: version "2.2.0" @@ -71,18 +71,18 @@ pg-types@2.2.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@8.16.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.16.0.tgz#40b08eedb5eb1834252cf3e3629503e32e6c6c04" - integrity sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg== +pg@8.20.0: + version "8.20.0" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.20.0.tgz#1a274de944cb329fd6dd77a6d371a005ba6b136d" + integrity sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA== dependencies: - pg-connection-string "^2.9.0" - pg-pool "^3.10.0" - pg-protocol "^1.10.0" + pg-connection-string "^2.12.0" + pg-pool "^3.13.0" + pg-protocol "^1.13.0" pg-types "2.2.0" pgpass "1.0.5" optionalDependencies: - pg-cloudflare "^1.2.5" + pg-cloudflare "^1.3.0" pgpass@1.0.5: version "1.0.5" diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 89f96974c123..0aebd074008d 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -602,7 +602,8 @@ export function createRunner(...paths: string[]) { if (process.env.DEBUG) log('stderr line', output); - if (ensureNoErrorOutput) { + // Ignore deprecation warnings for this purpose + if (ensureNoErrorOutput && !`${output}`.includes('DeprecationWarning:')) { complete(new Error(`Expected no error output but got: '${output}'`)); } }); diff --git a/packages/node-core/src/integrations/console.ts b/packages/node-core/src/integrations/console.ts index d85e00e6b9d7..86591871999e 100644 --- a/packages/node-core/src/integrations/console.ts +++ b/packages/node-core/src/integrations/console.ts @@ -39,7 +39,14 @@ export const consoleIntegration = defineIntegration((options: Partial => { - if (![16, 18, 20, 22, 24].includes(NODE_MAJOR)) { + if (![16, 18, 20, 22, 24, 26].includes(NODE_MAJOR)) { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( diff --git a/yarn.lock b/yarn.lock index d5e66586670c..78e5b0b5d356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7694,10 +7694,10 @@ detect-libc "^2.0.3" node-abi "^3.73.0" -"@sentry-internal/node-native-stacktrace@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/node-native-stacktrace/-/node-native-stacktrace-0.4.0.tgz#8f6e7a21537373a5623714c14d3350e1bb4602f0" - integrity sha512-cuRBBqnsHOJJqLCii9GvwedzjetsihIarq7TxCjgG88JyF8TZWRMlUBu/OogWhYZVU8uHqAeSvpbzolnmdhdkw== +"@sentry-internal/node-native-stacktrace@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/node-native-stacktrace/-/node-native-stacktrace-0.5.0.tgz#834a5326fd45a97d1abe3b6110e9b268c7a2fa4e" + integrity sha512-vi+yY8D0TgUdpd8ja2BPqm689N+WZPWfXNkx0fzKYlVRGymUpQeyUrz2b6dscYE8Qr3ZiA6sz8RtXeQy1r9ZTQ== dependencies: detect-libc "^2.0.4" node-abi "^3.89.0" From 9860cdc08508cf5021a69277d1152059a1829a70 Mon Sep 17 00:00:00 2001 From: "javascript-sdk-gitflow[bot]" <255134079+javascript-sdk-gitflow[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 16:04:36 +0200 Subject: [PATCH 20/32] chore: Add external contributor to CHANGELOG.md (#20805) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #20783 Co-authored-by: mydea <2411343+mydea@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8fa4804fef5..f833aee0029d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @dmmulroy. Thank you for your contribution! +Work in this release was contributed by @dmmulroy and @SAY-5. Thank you for your contributions! ## 10.52.0 From ad1f373f237d9a52a055048037d50c8b54b0cb86 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 12 May 2026 11:30:45 +0900 Subject: [PATCH 21/32] feat(core): Add `streamGenAiSpans` options to stream gen_ai spans (#20785) We have concluded the experiment of streaming gen_ai spans as v2 envelope items via our [10.50.0-alpha.0 release](https://github.com/getsentry/sentry-javascript/releases/tag/10.50.0-alpha.0) and are now ready to pull this into stable gated behind a flag. Adds a new experimental option `streamGenAiSpans` option that controls how `gen_ai` spans are sent to Sentry. When set, the SDK extracts all `gen_ai` spans out of a transaction and sends them as v2 envelope items. This avoids payload size limits of regular transactions. ```ts Sentry.init({ dsn: "https://examplePublicKey@o0.ingest.sentry.io/0", _experiments: { streamGenAiSpans: true, }, }); ``` --- CHANGELOG.md | 17 + .../suites/tracing/anthropic-ai/index.ts | 1 + .../suites/tracing/anthropic-ai/test.ts | 51 +- .../suites/tracing/google-genai/index.ts | 1 + .../suites/tracing/google-genai/test.ts | 113 +- .../suites/tracing/langchain/index.ts | 1 + .../suites/tracing/langchain/test.ts | 84 +- .../suites/tracing/langgraph/index.ts | 1 + .../suites/tracing/langgraph/test.ts | 82 +- .../suites/tracing/openai/index.ts | 1 + .../suites/tracing/openai/test.ts | 60 +- .../nextjs-15/sentry.server.config.ts | 1 + .../nextjs-15/tests/ai-error.test.ts | 16 +- .../nextjs-15/tests/ai-test.test.ts | 52 +- .../nextjs-16/sentry.server.config.ts | 1 + .../nextjs-16/tests/ai-error.test.ts | 16 +- .../nextjs-16/tests/ai-test.test.ts | 52 +- .../anthropic/instrument-no-truncation.mjs | 1 + .../instrument-streaming-with-truncation.mjs | 1 + .../anthropic/instrument-streaming.mjs | 1 + .../anthropic/instrument-with-options.mjs | 1 + .../tracing/anthropic/instrument-with-pii.mjs | 1 + .../suites/tracing/anthropic/instrument.mjs | 1 + .../suites/tracing/anthropic/test.ts | 1068 +++++----- .../google-genai/instrument-no-truncation.mjs | 1 + .../instrument-streaming-with-truncation.mjs | 1 + .../google-genai/instrument-streaming.mjs | 1 + .../google-genai/instrument-with-options.mjs | 1 + .../google-genai/instrument-with-pii.mjs | 1 + .../tracing/google-genai/instrument.mjs | 1 + .../suites/tracing/google-genai/test.ts | 900 ++++----- .../langchain/instrument-no-truncation.mjs | 1 + .../instrument-streaming-with-truncation.mjs | 1 + .../langchain/instrument-streaming.mjs | 1 + .../tracing/langchain/instrument-with-pii.mjs | 1 + .../suites/tracing/langchain/instrument.mjs | 1 + .../suites/tracing/langchain/test.ts | 712 +++---- .../langchain/v1/instrument-with-pii.mjs | 1 + .../tracing/langchain/v1/instrument.mjs | 1 + .../suites/tracing/langchain/v1/test.ts | 537 ++--- .../langgraph/instrument-no-truncation.mjs | 1 + .../instrument-streaming-with-truncation.mjs | 1 + .../langgraph/instrument-streaming.mjs | 1 + .../tracing/langgraph/instrument-with-pii.mjs | 1 + .../suites/tracing/langgraph/instrument.mjs | 1 + .../suites/tracing/langgraph/test.ts | 512 ++--- .../openai/instrument-no-truncation.mjs | 1 + .../tracing/openai/instrument-root-span.mjs | 1 + .../instrument-streaming-with-truncation.mjs | 1 + .../tracing/openai/instrument-streaming.mjs | 1 + .../openai/instrument-with-options.mjs | 1 + .../tracing/openai/instrument-with-pii.mjs | 1 + .../suites/tracing/openai/instrument.mjs | 1 + .../openai-tool-calls/instrument-with-pii.mjs | 1 + .../openai/openai-tool-calls/instrument.mjs | 1 + .../tracing/openai/openai-tool-calls/test.ts | 548 ++++-- .../suites/tracing/openai/test.ts | 1728 ++++++++++------- .../openai/v6/instrument-root-span.mjs | 1 + .../openai/v6/instrument-with-options.mjs | 1 + .../tracing/openai/v6/instrument-with-pii.mjs | 1 + .../suites/tracing/openai/v6/instrument.mjs | 1 + .../suites/tracing/openai/v6/test.ts | 1198 ++++++++---- .../vercelai/instrument-no-truncation.mjs | 1 + .../tracing/vercelai/instrument-with-pii.mjs | 1 + .../suites/tracing/vercelai/instrument.mjs | 1 + .../tracing/vercelai/test-generate-object.ts | 91 +- .../suites/tracing/vercelai/test.ts | 1163 ++++------- .../vercelai/v5/instrument-with-pii.mjs | 1 + .../suites/tracing/vercelai/v5/instrument.mjs | 1 + .../suites/tracing/vercelai/v5/test.ts | 671 ++----- .../vercelai/v6/instrument-with-pii.mjs | 1 + .../suites/tracing/vercelai/v6/instrument.mjs | 1 + .../suites/tracing/vercelai/v6/test.ts | 777 +++----- packages/core/src/client.ts | 9 + packages/core/src/tracing/sentrySpan.ts | 5 + .../src/tracing/spans/extractGenAiSpans.ts | 58 + .../tracing/spans/spanJsonToStreamedSpan.ts | 23 + packages/core/src/tracing/vercel-ai/index.ts | 7 +- packages/core/src/types-hoist/envelope.ts | 2 +- packages/core/src/types-hoist/options.ts | 9 + .../core/test/lib/tracing/sentrySpan.test.ts | 1 + .../tracing/spans/extractGenAiSpans.test.ts | 158 ++ .../spans/spanJsonToStreamedSpan.test.ts | 93 + packages/opentelemetry/src/spanExporter.ts | 30 +- 84 files changed, 5234 insertions(+), 5660 deletions(-) create mode 100644 packages/core/src/tracing/spans/extractGenAiSpans.ts create mode 100644 packages/core/src/tracing/spans/spanJsonToStreamedSpan.ts create mode 100644 packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts create mode 100644 packages/core/test/lib/tracing/spans/spanJsonToStreamedSpan.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f833aee0029d..b177648c7a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ Work in this release was contributed by @dmmulroy and @SAY-5. Thank you for your contributions! +### Important Changes + +- **feat(core): Add `streamGenAiSpans` options to stream gen_ai spans ([#20785](https://github.com/getsentry/sentry-javascript/pull/20785))** + + Adds a new `streamGenAiSpans` option that controls how `gen_ai` spans are + sent to Sentry. When set, the SDK extracts all `gen_ai` spans out of a + transaction and sends them as v2 envelope items. + + Enable this option if gen_ai spans are being dropped because the transaction payload exceeds size limits. + + ```ts + Sentry.init({ + dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', + streamGenAiSpans: true, + }); + ``` + ## 10.52.0 ### Important Changes diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts index 08df5e24b05b..9ff6a9406258 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/index.ts @@ -16,6 +16,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, + streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { 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 17cea5dbf95b..4f60868cddfb 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 @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { GEN_AI_OPERATION_NAME_ATTRIBUTE, @@ -21,30 +20,36 @@ it('traces a basic message creation request', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { + // Transaction item (first item in envelope) const transactionEvent = envelope[1]?.[0]?.[1] as any; - expect(transactionEvent.transaction).toBe('GET /'); - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_mock123', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - }), - ]), - ); + + // Span container item (second item in same envelope) + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); + + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat claude-3-haiku-20240307 + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.anthropic' }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'anthropic' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'claude-3-haiku-20240307', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'claude-3-haiku-20240307', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'msg_mock123' }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); }) .start(signal); await runner.makeRequest('get', '/'); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts index 88328768c6f2..ca6138ce17ef 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts @@ -16,6 +16,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, + streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { 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 5194e3d3a581..98f730af9716 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 @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { GEN_AI_OPERATION_NAME_ATTRIBUTE, @@ -18,67 +17,69 @@ import { createRunner } from '../../../runner'; // want to test that the instrumentation does not break in our // cloudflare SDK. -it('traces Google GenAI chat creation and message sending', async () => { +it('traces Google GenAI chat creation and message sending', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { + // Transaction item (first item in envelope) const transactionEvent = envelope[1]?.[0]?.[1] as any; - expect(transactionEvent.transaction).toBe('GET /'); - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - // chat.sendMessage - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'chat gemini-1.5-pro', - op: 'gen_ai.chat', - origin: 'auto.ai.google_genai', - }), - // models.generateContent - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - }), - // models.embedContent - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - }), - description: 'embeddings text-embedding-004', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - }), - ]), - ); + + // Span container item (second item in same envelope) + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat gemini-1.5-pro + expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gemini-1.5-pro', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + + // [1] generate_content gemini-1.5-flash + expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'generate_content', + }); + expect(secondSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.generate_content' }); + expect(secondSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gemini-1.5-flash', + }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]).toEqual({ type: 'double', value: 0.9 }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 100 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + + // [2] embeddings text-embedding-004 + expect(thirdSpan!.name).toBe('embeddings text-embedding-004'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings' }); + expect(thirdSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.embeddings' }); + expect(thirdSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-004', + }); }) - .start(); + .start(signal); await runner.makeRequest('get', '/'); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts index 0d59fd91c2b7..b1cd17c5eda1 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/index.ts @@ -9,6 +9,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, + streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { 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 d4abc4ae7220..d05255f2c437 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { GEN_AI_OPERATION_NAME_ATTRIBUTE, @@ -22,53 +21,46 @@ it('traces langchain chat model, chain, and tool invocations', async ({ signal } const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { + // Transaction item (first item in envelope) const transactionEvent = envelope[1]?.[0]?.[1] as any; - expect(transactionEvent.transaction).toBe('GET /'); - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - // Chat model span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - }), - // Chain span - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - 'langchain.chain.name': 'my_test_chain', - }), - description: 'chain my_test_chain', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langchain', - }), - // Tool span - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'search_tool', - }), - description: 'execute_tool search_tool', - op: 'gen_ai.execute_tool', - origin: 'auto.ai.langchain', - }), - ]), - ); + + // Span container item (second item in same envelope) + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat claude-3-5-sonnet-20241022 + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'anthropic' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'claude-3-5-sonnet-20241022', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 100 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + + // [1] chain my_test_chain + expect(secondSpan!.name).toBe('chain my_test_chain'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); + expect(secondSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' }); + expect(secondSpan!.attributes['langchain.chain.name']).toEqual({ type: 'string', value: 'my_test_chain' }); + + // [2] execute_tool search_tool + expect(thirdSpan!.name).toBe('execute_tool search_tool'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); + expect(thirdSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.execute_tool' }); + expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'search_tool' }); }) .start(signal); await runner.makeRequest('get', '/'); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts index 6837a14be111..c18ed4088e69 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/index.ts @@ -10,6 +10,7 @@ export default Sentry.withSentry( dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, sendDefaultPii: true, + streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts index 6efa07164df5..4c8623654273 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts @@ -1,11 +1,9 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { GEN_AI_AGENT_NAME_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_PIPELINE_NAME_ATTRIBUTE, - GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, @@ -22,48 +20,56 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { + // Transaction item (first item in envelope) const transactionEvent = envelope[1]?.[0]?.[1] as any; - expect(transactionEvent.transaction).toBe('GET /'); - // Check create_agent span - const createAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.create_agent'); - expect(createAgentSpan).toMatchObject({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - }, - description: 'create_agent weather_assistant', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - }); + // Span container item (second item in same envelope) + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); - // Check invoke_agent span - const invokeAgentSpan = transactionEvent.spans.find((span: any) => span.op === 'gen_ai.invoke_agent'); - expect(invokeAgentSpan).toMatchObject({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in SF?"}]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }), - description: 'invoke_agent weather_assistant', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] create_agent weather_assistant + expect(firstSpan!.name).toBe('create_agent weather_assistant'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'create_agent', + }); + expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.create_agent' }); + expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); + expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'weather_assistant', }); - // Verify tools are captured - if (invokeAgentSpan.data[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]) { - expect(invokeAgentSpan.data[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toMatch(/get_weather/); - } + // [1] invoke_agent weather_assistant + expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'invoke_agent', + }); + expect(secondSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' }); + expect(secondSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); + expect(secondSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'weather_assistant', + }); + expect(secondSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'weather_assistant', + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the weather in SF?"}]', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'mock-model' }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30 }); }) .start(signal); await runner.makeRequest('get', '/'); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts index 8b21b479ce80..0a50b60c43a6 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/index.ts @@ -15,6 +15,7 @@ export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, tracesSampleRate: 1.0, + streamGenAiSpans: true, }), { async fetch(_request, _env, _ctx) { 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 1c057e1a986c..76288214e9f2 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { expect, it } from 'vitest'; import { GEN_AI_OPERATION_NAME_ATTRIBUTE, @@ -23,32 +22,41 @@ it('traces a basic chat completion request', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { - const transactionEvent = envelope[1]?.[0]?.[1]; - + // Transaction item (first item in envelope) + const transactionEvent = envelope[1]?.[0]?.[1] as any; expect(transactionEvent.transaction).toBe('GET /'); - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-mock123', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - }), - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - }), - ]), - ); + + // Span container item (second item in same envelope) + const container = envelope[1]?.[1]?.[1] as any; + expect(container).toBeDefined(); + + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat gpt-3.5-turbo + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.openai' }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-mock123', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); }) .start(signal); await runner.makeRequest('get', '/'); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts index 270c7244ad17..e5306a66e579 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts @@ -11,4 +11,5 @@ Sentry.init({ bufferSize: 1000, }, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-error.test.ts index a8c39ec032ec..81bf9d04ba97 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-error.test.ts @@ -1,11 +1,16 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction, waitForError } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpans, waitForTransaction } from '@sentry-internal/test-utils'; test('should create AI spans with correct attributes and error linking', async ({ page }) => { const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent.transaction === 'GET /ai-error-test'; }); + // gen_ai spans are extracted into a separate span v2 envelope item + const genAiSpansPromise = waitForStreamedSpans('nextjs-15', spans => + spans.some(span => getSpanOp(span) === 'gen_ai.invoke_agent'), + ); + const errorEventPromise = waitForError('nextjs-15', async errorEvent => { return errorEvent.exception?.values?.[0]?.value?.includes('Tool call failed'); }); @@ -13,21 +18,20 @@ test('should create AI spans with correct attributes and error linking', async ( await page.goto('/ai-error-test'); const aiTransaction = await aiTransactionPromise; + const genAiSpans = await genAiSpansPromise; const errorEvent = await errorEventPromise; expect(aiTransaction).toBeDefined(); expect(aiTransaction.transaction).toBe('GET /ai-error-test'); - const spans = aiTransaction.spans || []; - // Each generateText call should create 2 spans: one for the pipeline and one for doGenerate // Plus a span for the tool call // TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working // because of this, only spans that are manually opted-in at call time will be captured // this may be fixed by https://github.com/vercel/ai/pull/6716 in the future - const aiPipelineSpans = spans.filter(span => span.op === 'gen_ai.invoke_agent'); - const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_content'); - const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool'); + const aiPipelineSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.invoke_agent'); + const aiGenerateSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.generate_content'); + const toolCallSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.execute_tool'); expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1); expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts index 42c21e4f8c80..b76ea6eb9ff9 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts @@ -1,29 +1,33 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans, waitForTransaction } from '@sentry-internal/test-utils'; test('should create AI spans with correct attributes', async ({ page }) => { const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent.transaction === 'GET /ai-test'; }); + // gen_ai spans are extracted into a separate span v2 envelope item + const genAiSpansPromise = waitForStreamedSpans('nextjs-15', spans => + spans.some(span => getSpanOp(span) === 'gen_ai.invoke_agent'), + ); + await page.goto('/ai-test'); const aiTransaction = await aiTransactionPromise; + const genAiSpans = await genAiSpansPromise; expect(aiTransaction).toBeDefined(); expect(aiTransaction.transaction).toBe('GET /ai-test'); - const spans = aiTransaction.spans || []; - // We expect spans for the first 3 AI calls (4th is disabled) // Each generateText call should create 2 spans: one for the pipeline and one for doGenerate // Plus a span for the tool call // TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working // because of this, only spans that are manually opted-in at call time will be captured // this may be fixed by https://github.com/vercel/ai/pull/6716 in the future - const aiPipelineSpans = spans.filter(span => span.op === 'gen_ai.invoke_agent'); - const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_content'); - const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool'); + const aiPipelineSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.invoke_agent'); + const aiGenerateSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.generate_content'); + const toolCallSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.execute_tool'); expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1); expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1); @@ -31,35 +35,35 @@ test('should create AI spans with correct attributes', async ({ page }) => { // First AI call - should have telemetry enabled and record inputs/outputs (sendDefaultPii: true) /* const firstPipelineSpan = aiPipelineSpans[0]; - expect(firstPipelineSpan?.data?.['vercel.ai.model.id']).toBe('mock-model-id'); - expect(firstPipelineSpan?.data?.['vercel.ai.model.provider']).toBe('mock-provider'); - expect(firstPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the first span?'); - expect(firstPipelineSpan?.data?.['gen_ai.output.messages']).toContain('First span here!'); - expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10); - expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */ + expect(firstPipelineSpan?.attributes?.['vercel.ai.model.id']?.value).toBe('mock-model-id'); + expect(firstPipelineSpan?.attributes?.['vercel.ai.model.provider']?.value).toBe('mock-provider'); + expect(firstPipelineSpan?.attributes?.['vercel.ai.prompt']?.value).toContain('Where is the first span?'); + expect(firstPipelineSpan?.attributes?.['gen_ai.output.messages']?.value).toContain('First span here!'); + expect(firstPipelineSpan?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe(10); + expect(firstPipelineSpan?.attributes?.['gen_ai.usage.output_tokens']?.value).toBe(20); */ // Second AI call - explicitly enabled telemetry const secondPipelineSpan = aiPipelineSpans[0]; - expect(secondPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the second span?'); - expect(secondPipelineSpan?.data?.['gen_ai.output.messages']).toContain('Second span here!'); + expect(secondPipelineSpan?.attributes?.['vercel.ai.prompt']?.value).toContain('Where is the second span?'); + expect(secondPipelineSpan?.attributes?.['gen_ai.output.messages']?.value).toContain('Second span here!'); // Third AI call - with tool calls /* const thirdPipelineSpan = aiPipelineSpans[2]; - expect(thirdPipelineSpan?.data?.['vercel.ai.response.finishReason']).toBe('tool-calls'); - expect(thirdPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(15); - expect(thirdPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(25); */ + expect(thirdPipelineSpan?.attributes?.['vercel.ai.response.finishReason']?.value).toBe('tool-calls'); + expect(thirdPipelineSpan?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe(15); + expect(thirdPipelineSpan?.attributes?.['gen_ai.usage.output_tokens']?.value).toBe(25); */ // Tool call span /* const toolSpan = toolCallSpans[0]; - expect(toolSpan?.data?.['vercel.ai.toolCall.name']).toBe('getWeather'); - expect(toolSpan?.data?.['vercel.ai.toolCall.id']).toBe('call-1'); - expect(toolSpan?.data?.['vercel.ai.toolCall.args']).toContain('San Francisco'); - expect(toolSpan?.data?.['vercel.ai.toolCall.result']).toContain('Sunny, 72°F'); */ + expect(toolSpan?.attributes?.['vercel.ai.toolCall.name']?.value).toBe('getWeather'); + expect(toolSpan?.attributes?.['vercel.ai.toolCall.id']?.value).toBe('call-1'); + expect(toolSpan?.attributes?.['vercel.ai.toolCall.args']?.value).toContain('San Francisco'); + expect(toolSpan?.attributes?.['vercel.ai.toolCall.result']?.value).toContain('Sunny, 72°F'); */ // Verify the fourth call was not captured (telemetry disabled) - const promptsInSpans = spans - .map(span => span.data?.['vercel.ai.prompt']) - .filter((prompt): prompt is string => prompt !== undefined); + const promptsInSpans = genAiSpans + .map(span => span.attributes?.['vercel.ai.prompt']?.value) + .filter((prompt): prompt is string => typeof prompt === 'string'); const hasDisabledPrompt = promptsInSpans.some(prompt => prompt.includes('Where is the third span?')); expect(hasDisabledPrompt).toBe(false); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts index d7015bce4a30..8428c75580ba 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts @@ -9,6 +9,7 @@ Sentry.init({ sendDefaultPii: true, // debug: true, integrations: [Sentry.vercelAIIntegration(), Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 })], + streamGenAiSpans: true, // Verify Log type is available beforeSendLog(log: Log) { return log; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-error.test.ts index 39e76bab0dde..62e6798773bd 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-error.test.ts @@ -1,11 +1,16 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction, waitForError } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpans, waitForTransaction } from '@sentry-internal/test-utils'; test('should create AI spans with correct attributes and error linking', async ({ page }) => { const aiTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => { return transactionEvent.transaction === 'GET /ai-error-test'; }); + // gen_ai spans are extracted into a separate span v2 envelope item + const genAiSpansPromise = waitForStreamedSpans('nextjs-16', spans => + spans.some(span => getSpanOp(span) === 'gen_ai.invoke_agent'), + ); + const errorEventPromise = waitForError('nextjs-16', async errorEvent => { return errorEvent.exception?.values?.[0]?.value?.includes('Tool call failed'); }); @@ -13,21 +18,20 @@ test('should create AI spans with correct attributes and error linking', async ( await page.goto('/ai-error-test'); const aiTransaction = await aiTransactionPromise; + const genAiSpans = await genAiSpansPromise; const errorEvent = await errorEventPromise; expect(aiTransaction).toBeDefined(); expect(aiTransaction.transaction).toBe('GET /ai-error-test'); - const spans = aiTransaction.spans || []; - // Each generateText call should create 2 spans: one for the pipeline and one for doGenerate // Plus a span for the tool call // TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working // because of this, only spans that are manually opted-in at call time will be captured // this may be fixed by https://github.com/vercel/ai/pull/6716 in the future - const aiPipelineSpans = spans.filter(span => span.op === 'gen_ai.invoke_agent'); - const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_content'); - const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool'); + const aiPipelineSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.invoke_agent'); + const aiGenerateSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.generate_content'); + const toolCallSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.execute_tool'); expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1); expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts index dcd129020035..89af644b1f21 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts @@ -1,29 +1,33 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans, waitForTransaction } from '@sentry-internal/test-utils'; test('should create AI spans with correct attributes', async ({ page }) => { const aiTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => { return transactionEvent.transaction === 'GET /ai-test'; }); + // gen_ai spans are extracted into a separate span v2 envelope item + const genAiSpansPromise = waitForStreamedSpans('nextjs-16', spans => + spans.some(span => getSpanOp(span) === 'gen_ai.invoke_agent'), + ); + await page.goto('/ai-test'); const aiTransaction = await aiTransactionPromise; + const genAiSpans = await genAiSpansPromise; expect(aiTransaction).toBeDefined(); expect(aiTransaction.transaction).toBe('GET /ai-test'); - const spans = aiTransaction.spans || []; - // We expect spans for the first 3 AI calls (4th is disabled) // Each generateText call should create 2 spans: one for the pipeline and one for doGenerate // Plus a span for the tool call // TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working // because of this, only spans that are manually opted-in at call time will be captured // this may be fixed by https://github.com/vercel/ai/pull/6716 in the future - const aiPipelineSpans = spans.filter(span => span.op === 'gen_ai.invoke_agent'); - const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_content'); - const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool'); + const aiPipelineSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.invoke_agent'); + const aiGenerateSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.generate_content'); + const toolCallSpans = genAiSpans.filter(span => getSpanOp(span) === 'gen_ai.execute_tool'); expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1); expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1); @@ -31,35 +35,35 @@ test('should create AI spans with correct attributes', async ({ page }) => { // First AI call - should have telemetry enabled and record inputs/outputs (sendDefaultPii: true) /* const firstPipelineSpan = aiPipelineSpans[0]; - expect(firstPipelineSpan?.data?.['vercel.ai.model.id']).toBe('mock-model-id'); - expect(firstPipelineSpan?.data?.['vercel.ai.model.provider']).toBe('mock-provider'); - expect(firstPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the first span?'); - expect(firstPipelineSpan?.data?.['gen_ai.output.messages']).toContain('First span here!'); - expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10); - expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */ + expect(firstPipelineSpan?.attributes?.['vercel.ai.model.id']?.value).toBe('mock-model-id'); + expect(firstPipelineSpan?.attributes?.['vercel.ai.model.provider']?.value).toBe('mock-provider'); + expect(firstPipelineSpan?.attributes?.['vercel.ai.prompt']?.value).toContain('Where is the first span?'); + expect(firstPipelineSpan?.attributes?.['gen_ai.output.messages']?.value).toContain('First span here!'); + expect(firstPipelineSpan?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe(10); + expect(firstPipelineSpan?.attributes?.['gen_ai.usage.output_tokens']?.value).toBe(20); */ // Second AI call - explicitly enabled telemetry const secondPipelineSpan = aiPipelineSpans[0]; - expect(secondPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the second span?'); - expect(secondPipelineSpan?.data?.['gen_ai.output.messages']).toContain('Second span here!'); + expect(secondPipelineSpan?.attributes?.['vercel.ai.prompt']?.value).toContain('Where is the second span?'); + expect(secondPipelineSpan?.attributes?.['gen_ai.output.messages']?.value).toContain('Second span here!'); // Third AI call - with tool calls /* const thirdPipelineSpan = aiPipelineSpans[2]; - expect(thirdPipelineSpan?.data?.['vercel.ai.response.finishReason']).toBe('tool-calls'); - expect(thirdPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(15); - expect(thirdPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(25); */ + expect(thirdPipelineSpan?.attributes?.['vercel.ai.response.finishReason']?.value).toBe('tool-calls'); + expect(thirdPipelineSpan?.attributes?.['gen_ai.usage.input_tokens']?.value).toBe(15); + expect(thirdPipelineSpan?.attributes?.['gen_ai.usage.output_tokens']?.value).toBe(25); */ // Tool call span /* const toolSpan = toolCallSpans[0]; - expect(toolSpan?.data?.['vercel.ai.toolCall.name']).toBe('getWeather'); - expect(toolSpan?.data?.['vercel.ai.toolCall.id']).toBe('call-1'); - expect(toolSpan?.data?.['vercel.ai.toolCall.args']).toContain('San Francisco'); - expect(toolSpan?.data?.['vercel.ai.toolCall.result']).toContain('Sunny, 72°F'); */ + expect(toolSpan?.attributes?.['vercel.ai.toolCall.name']?.value).toBe('getWeather'); + expect(toolSpan?.attributes?.['vercel.ai.toolCall.id']?.value).toBe('call-1'); + expect(toolSpan?.attributes?.['vercel.ai.toolCall.args']?.value).toContain('San Francisco'); + expect(toolSpan?.attributes?.['vercel.ai.toolCall.result']?.value).toContain('Sunny, 72°F'); */ // Verify the fourth call was not captured (telemetry disabled) - const promptsInSpans = spans - .map(span => span.data?.['vercel.ai.prompt']) - .filter((prompt): prompt is string => prompt !== undefined); + const promptsInSpans = genAiSpans + .map(span => span.attributes?.['vercel.ai.prompt']?.value) + .filter((prompt): prompt is string => typeof prompt === 'string'); const hasDisabledPrompt = promptsInSpans.some(prompt => prompt.includes('Where is the third span?')); expect(hasDisabledPrompt).toBe(false); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs index ce15aad4e8e1..52b29da36f16 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-no-truncation.mjs @@ -21,4 +21,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs index 9d8360708ab3..d72a9fce5121 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming-with-truncation.mjs @@ -13,4 +13,5 @@ Sentry.init({ enableTruncation: true, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs index 48a860c510c5..7db8f66f7c58 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-streaming.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, traceLifecycle: 'stream', + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs index bbbefef79148..37199cf9030b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs @@ -20,4 +20,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs index 8c6bbcc3ce0a..fe6a18ba32dc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs index 2b8a197791e2..41b37b7fc409 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index e740c24071fd..63a965345b25 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, @@ -30,305 +29,14 @@ describe('Anthropic integration', () => { const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { transaction: 'main', - spans: expect.arrayContaining([ - // First span - basic message completion without PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_mock123', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - // Second span - error handling - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'internal_error', - }), - // Third span - token counting (no response.text because recordOutputs=false by default) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - // Fourth span - models.retrieve - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'models', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.models', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - }), - description: 'models claude-3-haiku-20240307', - op: 'gen_ai.models', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - ]), }; const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { transaction: 'main', - spans: expect.arrayContaining([ - // First span - basic message completion with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the capital of France?"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_mock123', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from Anthropic mock!', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'http.request.method': 'POST', - 'http.request.method_original': 'POST', - 'http.response.status_code': 200, - 'otel.kind': 'CLIENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', - 'url.path': '/anthropic/v1/messages', - 'url.query': '', - 'url.scheme': 'http', - }), - op: 'http.client', - origin: 'auto.http.otel.node_fetch', - status: 'ok', - }), - - // Second - error handling with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"This will fail"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'internal_error', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'http.request.method': 'POST', - 'http.request.method_original': 'POST', - 'http.response.status_code': 404, - 'otel.kind': 'CLIENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', - 'url.path': '/anthropic/v1/messages', - 'url.query': '', - 'url.scheme': 'http', - }), - op: 'http.client', - origin: 'auto.http.otel.node_fetch', - status: 'not_found', - }), - - // Third - token counting with PII (response.text is present because sendDefaultPii=true enables recordOutputs) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the capital of France?"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: '15', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'http.request.method': 'POST', - 'http.request.method_original': 'POST', - 'http.response.status_code': 200, - 'otel.kind': 'CLIENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', - 'url.path': '/anthropic/v1/messages/count_tokens', - 'url.query': '', - 'url.scheme': 'http', - }), - op: 'http.client', - origin: 'auto.http.otel.node_fetch', - status: 'ok', - }), - - // Fourth - models.retrieve with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'models', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.models', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - }), - description: 'models claude-3-haiku-20240307', - op: 'gen_ai.models', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'http.request.method_original': 'GET', - 'http.response.status_code': 200, - 'otel.kind': 'CLIENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', - 'url.path': '/anthropic/v1/models/claude-3-haiku-20240307', - 'url.query': '', - 'url.scheme': 'http', - 'user_agent.original': 'Anthropic/JS 0.63.0', - }), - op: 'http.client', - origin: 'auto.http.otel.node_fetch', - status: 'ok', - }), - - // Fifth - messages.create with stream: true - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the capital of France?"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_stream123', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from stream!', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'http.request.method': 'POST', - 'http.request.method_original': 'POST', - 'http.response.status_code': 200, - 'otel.kind': 'CLIENT', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.node_fetch', - 'url.path': '/anthropic/v1/messages', - 'url.query': '', - 'url.scheme': 'http', - 'user_agent.original': 'Anthropic/JS 0.63.0', - }), - op: 'http.client', - origin: 'auto.http.otel.node_fetch', - status: 'ok', - }), - - // Sixth - messages.stream - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - ]), }; const EXPECTED_TRANSACTION_WITH_OPTIONS = { transaction: 'main', - spans: expect.arrayContaining([ - // Check that custom options are respected - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text when recordOutputs: true - }), - }), - // Check token counting with options - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: '15', // Present because recordOutputs=true is set in options - }), - op: 'gen_ai.chat', - }), - // Check models.retrieve with options - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'models', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - }), - op: 'gen_ai.models', - description: 'models claude-3-haiku-20240307', - }), - ]), }; const EXPECTED_MODEL_ERROR = { @@ -351,35 +59,92 @@ describe('Anthropic integration', () => { await createRunner() .ignore('event') .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] messages.create — basic message completion without PII + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [1] messages.create with error-model — error handling + expect(secondSpan!.name).toBe('chat error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + + // [2] messages.countTokens — token counting + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + + // [3] models.retrieve + expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('models'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + expect(fourthSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + }, + }) .start() .completed(); }); }); createEsmAndCjsTests(__dirname, 'scenario-with-response.mjs', 'instrument.mjs', (createRunner, test) => { - const chatSpan = (responseId: string) => - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: responseId, - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - status: 'ok', - }); - test('preserves .withResponse() and .asResponse() for non-streaming and streaming', async () => { await createRunner() .ignore('event') .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - chatSpan('msg_withresponse'), - chatSpan('msg_withresponse'), - chatSpan('msg_stream_withresponse'), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] .withResponse() — non-streaming + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_withresponse'); + + // [1] .asResponse() — non-streaming + expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_withresponse'); + + // [2] streaming .withResponse() + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_withresponse'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); }, }) .start() @@ -392,6 +157,37 @@ describe('Anthropic integration', () => { await createRunner() .expect({ event: EXPECTED_MODEL_ERROR }) .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ + span: container => { + expect(container.items).toHaveLength(5); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan] = container.items; + + // [0] messages.create — basic message completion + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); + + // [1] messages.create with error-model — error handling + expect(secondSpan!.name).toBe('chat error-model'); + expect(secondSpan!.status).toBe('error'); + + // [2] messages.countTokens — token counting + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + // [3] models.retrieve + expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + + // [4] messages.create stream: true + messages.stream (both share this span due to pre-existing bug) + expect(fifthSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream123'); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + }, + }) .expect({ event: EXPECTED_STREAM_EVENT_HANDLER_MESSAGE }) .start() .completed(); @@ -403,6 +199,66 @@ describe('Anthropic integration', () => { await createRunner() .expect({ event: EXPECTED_MODEL_ERROR }) .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ + span: container => { + expect(container.items).toHaveLength(5); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan] = container.items; + + // [0] messages.create — basic message completion with PII + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"What is the capital of France?"}]', + ); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from Anthropic mock!'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + + // [1] messages.create with error-model — error handling with PII + expect(secondSpan!.name).toBe('chat error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"This will fail"}]', + ); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + + // [2] messages.countTokens — token counting with PII (response text records input_tokens as "15") + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('15'); + + // [3] models.retrieve with PII + expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + + // [4] messages.create stream: true + messages.stream (both share this span due to pre-existing bug) + // TODO: messages.stream() should produce its own distinct gen_ai span, but it + // currently does not (pre-existing bug). Once fixed, add an additional indexed span assertion. + expect(fifthSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream123'); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + }, + }) .expect({ event: EXPECTED_STREAM_EVENT_HANDLER_MESSAGE }) .start() .completed(); @@ -414,6 +270,48 @@ describe('Anthropic integration', () => { await createRunner() .expect({ event: EXPECTED_MODEL_ERROR }) .expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }) + .expect({ + span: container => { + expect(container.items).toHaveLength(5); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan] = container.items; + + // [0] messages.create — chat span with custom PII options (input messages + response text recorded) + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); + + // [1] messages.create with error-model — error handling + expect(secondSpan!.name).toBe('chat error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + // [2] messages.countTokens — token counting with options (response text = "15") + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('15'); + + // [3] models.retrieve with options + expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('models'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + + // [4] messages.create stream: true + messages.stream (share this span due to pre-existing bug) + expect(fifthSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream123'); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + }, + }) .expect({ event: EXPECTED_STREAM_EVENT_HANDLER_MESSAGE }) .start() .completed(); @@ -422,101 +320,98 @@ describe('Anthropic integration', () => { const EXPECTED_STREAM_SPANS_PII_FALSE = { transaction: 'main', - spans: expect.arrayContaining([ - // messages.create with stream: true - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_stream_1', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["end_turn"]', - }), - }), - // messages.stream - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_stream_1', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - }), - // messages.stream with redundant stream: true param - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'msg_stream_1', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - }), - ]), }; const EXPECTED_STREAM_SPANS_PII_TRUE = { transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - // streamed text concatenated - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from stream!', - }), - }), - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from stream!', - }), - }), - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from stream!', - }), - }), - ]), }; createEsmAndCjsTests(__dirname, 'scenario-stream.mjs', 'instrument.mjs', (createRunner, test) => { test('streams produce spans with token usage and metadata (PII false)', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_STREAM_SPANS_PII_FALSE }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_STREAM_SPANS_PII_FALSE }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] messages.create with stream: true + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["end_turn"]'); + + // [1] messages.stream (no request.stream attribute — distinguishes from the other two) + expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [2] messages.stream with redundant stream: true param + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); + }, + }) + .start() + .completed(); }); }); createEsmAndCjsTests(__dirname, 'scenario-stream.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('streams record response text when PII true', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_STREAM_SPANS_PII_TRUE }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_STREAM_SPANS_PII_TRUE }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] messages.create with stream: true — response text captured with PII + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + + // [1] messages.stream — response text captured with PII, no request.stream param + expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + + // [2] messages.stream with redundant stream: true — response text captured with PII + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + }, + }) + .start() + .completed(); }); }); @@ -530,16 +425,19 @@ describe('Anthropic integration', () => { await createRunner() .ignore('event') .expect({ - transaction: { - spans: expect.arrayContaining([ - expect.objectContaining({ - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_TOOLS_JSON, - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: EXPECTED_TOOL_CALLS_JSON, - }), - }), - ]), + transaction: {}, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] messages.create with tools — available tools + tool calls recorded with PII + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe(EXPECTED_TOOLS_JSON); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toBe(EXPECTED_TOOL_CALLS_JSON); }, }) .start() @@ -557,17 +455,27 @@ describe('Anthropic integration', () => { await createRunner() .ignore('event') .expect({ - transaction: { - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - data: expect.objectContaining({ - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_TOOLS_JSON, - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: EXPECTED_TOOL_CALLS_JSON, - }), - }), - ]), + transaction: {}, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] messages.create(stream: true) with tools — available tools + tool calls recorded with PII + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe(EXPECTED_TOOLS_JSON); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toBe(EXPECTED_TOOL_CALLS_JSON); + + // [1] messages.stream — currently records as error since messages.stream doesn't get + // iterable semantics through the mock; this preserves observed behavior. + expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); }, }) .start() @@ -578,97 +486,85 @@ describe('Anthropic integration', () => { // Additional error scenarios - Streaming errors const EXPECTED_STREAM_ERROR_SPANS = { transaction: 'main', - spans: expect.arrayContaining([ - // Error with messages.create on stream initialization - expect.objectContaining({ - description: 'chat error-stream-init', - op: 'gen_ai.chat', - status: 'internal_error', // Actual status coming from the instrumentation - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-stream-init', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - }), - }), - // Error with messages.stream on stream initialization - expect.objectContaining({ - description: 'chat error-stream-init', - op: 'gen_ai.chat', - status: 'internal_error', // Actual status coming from the instrumentation - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-stream-init', - }), - }), - // Error midway with messages.create on streaming - note: The stream is started successfully - // so we get a successful span with the content that was streamed before the error - expect.objectContaining({ - description: 'chat error-stream-midway', - op: 'gen_ai.chat', - status: 'ok', - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-stream-midway', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'This stream will ', // We received some data before error - }), - }), - // Error midway with messages.stream - same behavior, we get a span with the streamed data - expect.objectContaining({ - description: 'chat error-stream-midway', - op: 'gen_ai.chat', - status: 'ok', - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-stream-midway', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'This stream will ', // We received some data before error - }), - }), - ]), }; createEsmAndCjsTests(__dirname, 'scenario-stream-errors.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('handles streaming errors correctly', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_STREAM_ERROR_SPANS }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_STREAM_ERROR_SPANS }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] messages.create(stream: true) error on stream init + expect(firstSpan!.name).toBe('chat error-stream-init'); + expect(firstSpan!.status).toBe('error'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-init'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + + // [1] messages.stream error on stream init (no request.stream param) + expect(secondSpan!.name).toBe('chat error-stream-init'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-init'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + + // [2] messages.create(stream: true) midway error — finishes 'ok' with partial text + expect(thirdSpan!.name).toBe('chat error-stream-midway'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-midway'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('This stream will '); + + // [3] messages.stream midway error — errors out + expect(fourthSpan!.name).toBe('chat error-stream-midway'); + expect(fourthSpan!.status).toBe('error'); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-midway'); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + }, + }) + .start() + .completed(); }); }); // Additional error scenarios - Tool errors and model retrieval errors const EXPECTED_ERROR_SPANS = { transaction: 'main', - spans: expect.arrayContaining([ - // Invalid tool format error - expect.objectContaining({ - description: 'chat invalid-format', - op: 'gen_ai.chat', - status: 'internal_error', - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'invalid-format', - }), - }), - // Model retrieval error - expect.objectContaining({ - description: 'models nonexistent-model', - op: 'gen_ai.models', - status: 'internal_error', - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'nonexistent-model', - }), - }), - // Successful tool usage (for comparison) - expect.objectContaining({ - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - status: 'ok', - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.stringContaining('tool_ok_1'), - }), - }), - ]), }; createEsmAndCjsTests(__dirname, 'scenario-errors.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('handles tool errors and model retrieval errors correctly', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_ERROR_SPANS }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_ERROR_SPANS }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] messages.create with invalid-format — tool format error + expect(firstSpan!.name).toBe('chat invalid-format'); + expect(firstSpan!.status).toBe('error'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('invalid-format'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + // [1] models.retrieve('nonexistent-model') — model retrieval error + expect(secondSpan!.name).toBe('models nonexistent-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('nonexistent-model'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + + // [2] Successful tool usage (for comparison) + expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toContain('tool_ok_1'); + }, + }) + .start() + .completed(); }); }); @@ -683,44 +579,39 @@ describe('Anthropic integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - // First call: Last message is large and gets truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - // Messages should be present (truncation happened) and should be a JSON array - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/), - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - // Second call: Last message is small and kept without truncation - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - // Small message should be kept intact - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]), - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] First call: last message is large and truncated (only C's remain, D's cropped) + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","content":"C+"\}\]$/, + ); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + + // [1] Second call: last message is small and kept without truncation + const smallMsgValue = JSON.stringify([ + { role: 'user', content: 'This is a small message that fits within the limit' }, + ]); + expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(smallMsgValue); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() @@ -731,43 +622,43 @@ describe('Anthropic integration', () => { createEsmAndCjsTests(__dirname, 'scenario-media-truncation.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('truncates media attachment, keeping all other details', async () => { + const expectedMediaMessages = JSON.stringify([ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: '[Blob substitute]', + }, + }, + ], + }, + ]); await createRunner() .ignore('event') .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-haiku-20240307', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - // Only the last message (with filtered media) should be kept - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { - role: 'user', - content: [ - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: '[Blob substitute]', - }, - }, - ], - }, - ]), - }), - description: 'chat claude-3-haiku-20240307', - op: 'gen_ai.chat', - origin: 'auto.ai.anthropic', - status: 'ok', - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] messages.create with media attachment — image data replaced, other fields preserved + expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedMediaMessages); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); }, }) .start() @@ -781,20 +672,21 @@ describe('Anthropic integration', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('extracts system instructions from messages', async () => { + const expectedInstructions = JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]); await createRunner() .ignore('event') .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant' }, - ]), - }), - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] messages.create — system instructions extracted into dedicated attribute + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe(expectedInstructions); }, }) .start() @@ -808,26 +700,6 @@ describe('Anthropic integration', () => { const EXPECTED_TRANSACTION_NO_TRUNCATION = { transaction: 'main', - spans: expect.arrayContaining([ - // Multiple messages should all be preserved (no popping to last message only) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - }), - // Long string input should not be truncated (messagesFromParams wraps it in an array) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([longStringInput]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - }), - }), - ]), }; createEsmAndCjsTests( @@ -836,9 +708,29 @@ describe('Anthropic integration', () => { 'instrument-no-truncation.mjs', (createRunner, test) => { test('does not truncate input messages when enableTruncation is false', async () => { + const expectedAllMessages = JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]); + const expectedLongString = JSON.stringify([longStringInput]); await createRunner() .ignore('event') .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] messages.create with multi-message conversation — all messages preserved + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedAllMessages); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + + // [1] messages.create with long string input — not truncated + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedLongString); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs index be5288b429d6..164490759932 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-no-truncation.mjs @@ -14,4 +14,5 @@ Sentry.init({ enableTruncation: false, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs index e706163aea04..8238ba744fcb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming-with-truncation.mjs @@ -13,4 +13,5 @@ Sentry.init({ enableTruncation: true, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs index 48a860c510c5..7db8f66f7c58 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-streaming.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, traceLifecycle: 'stream', + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs index 9823f5680be3..03cef86fecbd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-options.mjs @@ -20,4 +20,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs index fa0a1136283d..7b17df2fce7f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument-with-pii.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs index 9bcfb96ac103..6c57f1abed6b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/instrument.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 9839ef5fa2c0..508f52331bf1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -1,10 +1,9 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, @@ -29,144 +28,51 @@ describe('Google GenAI integration', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // chat.sendMessage (should get model from context) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', // Should get from chat context - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }, - description: 'chat gemini-1.5-pro', - op: 'gen_ai.chat', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // models.generateContent - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }, - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // error handling - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }, - description: 'generate_content error-model', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // chat.sendMessage with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include message when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response when recordOutputs: true - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'chat gemini-1.5-pro', - op: 'gen_ai.chat', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // models.generateContent with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response when recordOutputs: true - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // error handling with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents when recordInputs: true - }), - description: 'generate_content error-model', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_WITH_OPTIONS = { - transaction: 'main', - spans: expect.arrayContaining([ - // Check that custom options are respected - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text when recordOutputs: true - }), - description: expect.not.stringContaining('stream-response'), // Non-streaming span - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates google genai related spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat.sendMessage (should get model from context) + expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.google_genai'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + + // [1] models.generateContent + expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-flash'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + + // [2] error handling + expect(thirdSpan!.name).toBe('generate_content error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + }, + }) .start() .completed(); }); @@ -176,7 +82,44 @@ describe('Google GenAI integration', () => { test('creates google genai related spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat.sendMessage with PII + expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + + // [1] models.generateContent with PII + expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + + // [2] error handling with PII + expect(thirdSpan!.name).toBe('generate_content error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + }, + }) .start() .completed(); }); @@ -186,7 +129,27 @@ describe('Google GenAI integration', () => { test('creates google genai related spans with custom options', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat.sendMessage with custom options (PII enabled via recordInputs/recordOutputs) + expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + + // [1] models.generateContent with custom options + expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + + // [2] error handling with custom options + expect(thirdSpan!.name).toBe('generate_content error-model'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + }, + }) .start() .completed(); }); @@ -195,242 +158,113 @@ describe('Google GenAI integration', () => { const EXPECTED_AVAILABLE_TOOLS_JSON = '[{"name":"controlLight","parametersJsonSchema":{"type":"object","properties":{"brightness":{"type":"number"},"colorTemperature":{"type":"string"}},"required":["brightness","colorTemperature"]}}]'; - const EXPECTED_TRANSACTION_TOOLS = { - transaction: 'main', - spans: expect.arrayContaining([ - // Non-streaming with tools - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-2.0-flash-001', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), // Should include tool calls - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 23, - }), - description: 'generate_content gemini-2.0-flash-001', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // Streaming with tools - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-2.0-flash-001', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), // Should include tool calls - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'mock-response-tools-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gemini-2.0-flash-001', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 22, - }), - description: 'generate_content gemini-2.0-flash-001', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // Without tools for comparison - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-2.0-flash-001', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'generate_content gemini-2.0-flash-001', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-options.mjs', (createRunner, test) => { test('creates google genai related spans with tool calls', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_TOOLS }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] Non-streaming with tools + expect(firstSpan!.name).toBe('generate_content gemini-2.0-flash-001'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe( + EXPECTED_AVAILABLE_TOOLS_JSON, + ); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toBeUndefined(); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(23); + + // [1] Streaming with tools + expect(secondSpan!.name).toBe('generate_content gemini-2.0-flash-001'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe( + EXPECTED_AVAILABLE_TOOLS_JSON, + ); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-tools-id'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-2.0-flash-001'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(22); + + // [2] Without tools for comparison + expect(thirdSpan!.name).toBe('generate_content gemini-2.0-flash-001'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(thirdSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + }, + }) + .start() + .completed(); }); }); - const EXPECTED_TRANSACTION_STREAMING = { - transaction: 'main', - spans: expect.arrayContaining([ - // models.generateContentStream (streaming) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'mock-response-streaming-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["STOP"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 22, - }), - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // chat.sendMessageStream (streaming) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'mock-response-streaming-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - }), - description: 'chat gemini-1.5-pro', - op: 'gen_ai.chat', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // blocked content streaming - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - }), - description: 'generate_content blocked-model', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - // error handling for streaming - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - }), - description: 'generate_content error-model', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_STREAMING_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // models.generateContentStream (streaming) with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents when recordInputs: true - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'mock-response-streaming-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["STOP"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 22, - }), - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // chat.sendMessageStream (streaming) with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include message when recordInputs: true - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'mock-response-streaming-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gemini-1.5-pro', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["STOP"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 22, - }), - description: 'chat gemini-1.5-pro', - op: 'gen_ai.chat', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // blocked content stream with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'blocked-model', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents when recordInputs: true - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }), - description: 'generate_content blocked-model', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - // error handling for streaming with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include contents when recordInputs: true - }), - description: 'generate_content error-model', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-streaming.mjs', 'instrument.mjs', (createRunner, test) => { test('creates google genai streaming spans with sendDefaultPii: false', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_STREAMING }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] models.generateContentStream (streaming) + expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-streaming-id'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(22); + + // [1] chat.sendMessageStream (streaming) + expect(secondSpan!.name).toBe('chat gemini-1.5-pro'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-streaming-id'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + + // [2] blocked content streaming + expect(thirdSpan!.name).toBe('generate_content blocked-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + + // [3] error handling for streaming + expect(fourthSpan!.name).toBe('generate_content error-model'); + expect(fourthSpan!.status).toBe('error'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + }, + }) + .start() + .completed(); }); }); @@ -438,7 +272,47 @@ describe('Google GenAI integration', () => { test('creates google genai streaming spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_STREAMING_PII_TRUE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] models.generateContentStream (streaming) with PII + expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); + + // [1] chat.sendMessageStream (streaming) with PII + expect(secondSpan!.name).toBe('chat gemini-1.5-pro'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); + + // [2] blocked content stream with PII + expect(thirdSpan!.name).toBe('generate_content blocked-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + + // [3] error handling for streaming with PII + expect(fourthSpan!.name).toBe('generate_content error-model'); + expect(fourthSpan!.status).toBe('error'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + }, + }) .start() .completed(); }); @@ -452,52 +326,34 @@ describe('Google GenAI integration', () => { test('truncates messages when they exceed byte limit - keeps only last message and crops it', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - // First call: Last message is large and gets truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - // Messages should be present (truncation happened) and should be a JSON array with parts - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching( - /^\[\{"role":"user","parts":\[\{"text":"C+"\}\]\}\]$/, - ), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // Second call: Last message is small and kept without truncation - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-flash', - // Small message should be kept intact - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { - role: 'user', - parts: [{ text: 'This is a small message that fits within the limit' }], - }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - description: 'generate_content gemini-1.5-flash', - op: 'gen_ai.generate_content', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - ]), + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] First call: Last message is large and gets truncated (only C's remain, D's are cropped) + expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","parts":\[\{"text":"C+"\}\]\}\]$/, + ); + + // [1] Second call: Last message is small and kept without truncation + expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + JSON.stringify([ + { + role: 'user', + parts: [{ text: 'This is a small message that fits within the limit' }], + }, + ]), + ); }, }) .start() @@ -514,18 +370,18 @@ describe('Google GenAI integration', () => { test('extracts system instructions from messages', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant' }, - ]), - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] generate_content with system instructions extracted + expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( + JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), + ); }, }) .start() @@ -534,111 +390,39 @@ describe('Google GenAI integration', () => { }, ); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - embedContent with string contents - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - }, - description: 'embeddings text-embedding-004', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // Second span - embedContent error model - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }, - description: 'embeddings error-model', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - // Third span - embedContent with array contents - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - }, - description: 'embeddings text-embedding-004', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - embedContent with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'What is the capital of France?', - }, - description: 'embeddings text-embedding-004', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - // Second span - embedContent error model with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'This will fail', - }, - description: 'embeddings error-model', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - status: 'internal_error', - }), - // Third span - embedContent with array contents and PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: - '[{"role":"user","parts":[{"text":"First input text"}]},{"role":"user","parts":[{"text":"Second input text"}]}]', - }, - description: 'embeddings text-embedding-004', - op: 'gen_ai.embeddings', - origin: 'auto.ai.google_genai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates google genai embeddings spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embedContent with string contents (no PII) + expect(firstSpan!.name).toBe('embeddings text-embedding-004'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.google_genai'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('text-embedding-004'); + expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toBeUndefined(); + + // [1] embedContent error model + expect(secondSpan!.name).toBe('embeddings error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + + // [2] embedContent with array contents (no PII) + expect(thirdSpan!.name).toBe('embeddings text-embedding-004'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + }, + }) .start() .completed(); }); @@ -648,7 +432,36 @@ describe('Google GenAI integration', () => { test('creates google genai embeddings spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embedContent with string contents and PII + expect(firstSpan!.name).toBe('embeddings text-embedding-004'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( + 'What is the capital of France?', + ); + + // [1] embedContent error model with PII + expect(secondSpan!.name).toBe('embeddings error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('This will fail'); + + // [2] embedContent with array contents and PII + expect(thirdSpan!.name).toBe('embeddings text-embedding-004'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(thirdSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( + '[{"role":"user","parts":[{"text":"First input text"}]},{"role":"user","parts":[{"text":"Second input text"}]}]', + ); + }, + }) .start() .completed(); }); @@ -656,22 +469,6 @@ describe('Google GenAI integration', () => { const longContent = 'A'.repeat(50_000); - const EXPECTED_TRANSACTION_NO_TRUNCATION = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', parts: [{ text: longContent }] }, - { role: 'model', parts: [{ text: 'Some reply' }] }, - { role: 'user', parts: [{ text: 'Follow-up question' }] }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-no-truncation.mjs', @@ -680,7 +477,24 @@ describe('Google GenAI integration', () => { test('does not truncate input messages when enableTruncation is false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] generate_content with full (non-truncated) input messages + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + JSON.stringify([ + { role: 'user', parts: [{ text: longContent }] }, + { role: 'model', parts: [{ text: 'Some reply' }] }, + { role: 'user', parts: [{ text: 'Follow-up question' }] }, + ]), + ); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs index 027299eeacad..8d9a5b45f919 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-no-truncation.mjs @@ -21,4 +21,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs index cdfebbf845fc..081d0969e00b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming-with-truncation.mjs @@ -13,4 +13,5 @@ Sentry.init({ enableTruncation: true, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs index 48a860c510c5..7db8f66f7c58 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-streaming.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, traceLifecycle: 'stream', + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs index 84212d887ee7..2a82a27dbdb4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument-with-pii.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs index 1fb023b535d4..1d36ee5c06e6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/instrument.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index f85e3187ac78..53135efea5cb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -1,8 +1,7 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { - GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, + GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, @@ -29,146 +28,56 @@ describe('LangChain integration', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - chat model with claude-3-5-sonnet - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second span - chat model with claude-3-opus - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-opus-20240229', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.95, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 200, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - }), - description: 'chat claude-3-opus-20240229', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third span - error handling - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - chat model with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response when recordOutputs: true - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second span - chat model with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-opus-20240229', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.95, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 200, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response when recordOutputs: true - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - description: 'chat claude-3-opus-20240229', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third span - error handling with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'internal_error', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain related spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat model with claude-3-5-sonnet + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + + // [1] chat model with claude-3-opus + expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [2] error handling + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + }, + }) .start() .completed(); }); @@ -176,14 +85,23 @@ describe('LangChain integration', () => { test('does not create duplicate spans from double module patching', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: event => { - const spans = event.spans || []; - const genAiChatSpans = spans.filter(span => span.op === 'gen_ai.chat'); + span: container => { // The scenario makes 3 LangChain calls (2 successful + 1 error). // Without the dedup guard, the file-level and module-level hooks // both patch the same prototype, producing 6 spans instead of 3. - expect(genAiChatSpans).toHaveLength(3); + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat claude-3-5-sonnet + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + // [1] chat claude-3-opus + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + // [2] chat error-model + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); }, }) .start() @@ -195,107 +113,88 @@ describe('LangChain integration', () => { test('creates langchain related spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat model with PII + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [1] chat model with PII + expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [2] error handling with PII + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + }, + }) .start() .completed(); }); }); - const EXPECTED_TRANSACTION_TOOL_CALLS = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 50, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: 'tool_use', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument.mjs', (createRunner, test) => { test('creates langchain spans with tool calls', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_TOOL_CALLS }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat with tool_use stop reason + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(150); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(50); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); + }, + }) + .start() + .completed(); }); }); - const EXPECTED_TRANSACTION_MESSAGE_TRUNCATION = { - transaction: 'main', - spans: expect.arrayContaining([ - // First call: String input truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - // Messages should be present and should include truncated string input (contains only Cs) - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second call: Array input, last message truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.any(String), - // Messages should be present (truncation happened) and should be a JSON array of a single index (contains only Cs) - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third call: Last message is small and kept without truncation - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.any(String), - // Small message should be kept intact - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-message-truncation.mjs', @@ -304,7 +203,36 @@ describe('LangChain integration', () => { test('truncates messages when they exceed byte limit', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_MESSAGE_TRUNCATION }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] String input truncated (only C's remain, D's are cropped) + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","content":"C+"\}\]$/, + ); + + // [1] Array input, last message truncated (only C's remain, D's are cropped) + expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","content":"C+"\}\]$/, + ); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); + + // [2] Last message is small and kept without truncation + expect(thirdSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), + ); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); + }, + }) .start() .completed(); }); @@ -319,46 +247,25 @@ describe('LangChain integration', () => { test('demonstrates timing issue with duplicate spans (ESM only)', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: event => { - // This test highlights the limitation: if a user creates an Anthropic client - // before importing LangChain, that client will still be instrumented and - // could cause duplicate spans when used alongside LangChain. - - const spans = event.spans || []; - - // First call: Direct Anthropic call made BEFORE LangChain import - // This should have Anthropic instrumentation (origin: 'auto.ai.anthropic') - const firstAnthropicSpan = spans.find( - span => span.description === 'chat claude-3-5-sonnet-20241022' && span.origin === 'auto.ai.anthropic', - ); - - // Second call: LangChain call - // This should have LangChain instrumentation (origin: 'auto.ai.langchain') - const langchainSpan = spans.find( - span => span.description === 'chat claude-3-5-sonnet-20241022' && span.origin === 'auto.ai.langchain', - ); - - // Third call: Direct Anthropic call made AFTER LangChain import - // This should NOT have Anthropic instrumentation (skip works correctly) - // Count how many Anthropic spans we have - should be exactly 1 - const anthropicSpans = spans.filter( - span => span.description === 'chat claude-3-5-sonnet-20241022' && span.origin === 'auto.ai.anthropic', - ); + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; // Verify the edge case limitation: - // - First Anthropic client (created before LangChain) IS instrumented - expect(firstAnthropicSpan).toBeDefined(); - expect(firstAnthropicSpan?.origin).toBe('auto.ai.anthropic'); - - // - LangChain call IS instrumented by LangChain - expect(langchainSpan).toBeDefined(); - expect(langchainSpan?.origin).toBe('auto.ai.langchain'); - - // - Second Anthropic client (created after LangChain) is NOT instrumented - // This demonstrates that the skip mechanism works for NEW clients - // We should only have ONE Anthropic span (the first one), not two - expect(anthropicSpans).toHaveLength(1); + // [0] Direct Anthropic call made BEFORE LangChain import — IS instrumented + // by Anthropic (origin: 'auto.ai.anthropic'). + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + + // [1] LangChain call — IS instrumented by LangChain (origin: 'auto.ai.langchain'). + expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + + // Third call (not present): Direct Anthropic call made AFTER LangChain import + // is NOT instrumented, which demonstrates the skip mechanism works for NEW + // clients. We should only have ONE Anthropic span (the first one), not two. }, }) .start() @@ -377,18 +284,18 @@ describe('LangChain integration', () => { test('extracts system instructions from messages', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant' }, - ]), - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat with extracted system instructions + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( + JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), + ); }, }) .start() @@ -401,32 +308,32 @@ describe('LangChain integration', () => { test('uses runName for chain spans instead of unknown_chain', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - description: 'chain format_prompt', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langchain', - data: expect.objectContaining({ - 'langchain.chain.name': 'format_prompt', - }), - }), - expect.objectContaining({ - description: 'chain parse_output', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langchain', - data: expect.objectContaining({ - 'langchain.chain.name': 'parse_output', - }), - }), - expect.objectContaining({ - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - }), - ]), + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] format_prompt chain (invoke_agent) + expect(firstSpan!.name).toBe('chain format_prompt'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes['langchain.chain.name'].value).toBe('format_prompt'); + + // [1] chat model invoked inside the chain + expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + + // [2] parse_output chain (invoke_agent) + expect(thirdSpan!.name).toBe('chain parse_output'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(thirdSpan!.attributes['langchain.chain.name'].value).toBe('parse_output'); + + // [3] unknown_chain (fallback name) + expect(fourthSpan!.name).toBe('chain unknown_chain'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); }, }) .start() @@ -438,101 +345,60 @@ describe('LangChain integration', () => { // Embeddings tests // ========================================================================= - const EXPECTED_TRANSACTION_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // embedQuery span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]: 1536, - }), - description: 'embeddings text-embedding-3-small', - op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - origin: 'auto.ai.langchain', - status: 'ok', - }), - // embedDocuments span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - }), - description: 'embeddings text-embedding-3-small', - op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Error span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }), - description: 'embeddings error-model', - op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - origin: 'auto.ai.langchain', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_EMBEDDINGS_PII = { - transaction: 'main', - spans: expect.arrayContaining([ - // embedQuery span with input recorded - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'Hello world', - }), - description: 'embeddings text-embedding-3-small', - op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - origin: 'auto.ai.langchain', - status: 'ok', - }), - // embedDocuments span with input recorded - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: JSON.stringify(['First document', 'Second document']), - }), - description: 'embeddings text-embedding-3-small', - op: GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE, - origin: 'auto.ai.langchain', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding spans with sendDefaultPii: false', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_EMBEDDINGS }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embedQuery span + expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('text-embedding-3-small'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); + + // [1] embedDocuments span + expect(secondSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + + // [2] Error span + expect(thirdSpan!.name).toBe('embeddings error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + }, + }) + .start() + .completed(); }); test('does not create duplicate embedding spans from double module patching', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: event => { - const spans = event.spans || []; - const embeddingSpans = spans.filter(span => span.op === GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + span: container => { // The scenario makes 3 embedding calls (2 successful + 1 error). - expect(embeddingSpans).toHaveLength(3); + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embedQuery + expect(firstSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + + // [1] embedDocuments + expect(secondSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + + // [2] error embedding call + expect(thirdSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); }, }) .start() @@ -544,7 +410,30 @@ describe('LangChain integration', () => { test('creates embedding spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_EMBEDDINGS_PII }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embedQuery span with input recorded + expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('Hello world'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); + + // [1] embedDocuments span with input recorded + expect(secondSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( + JSON.stringify(['First document', 'Second document']), + ); + + // [2] error embedding span (input still recorded with PII) + expect(thirdSpan!.name).toBe('embeddings error-model'); + expect(thirdSpan!.status).toBe('error'); + }, + }) .start() .completed(); }); @@ -552,22 +441,6 @@ describe('LangChain integration', () => { const longContent = 'A'.repeat(50_000); - const EXPECTED_TRANSACTION_NO_TRUNCATION = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-no-truncation.mjs', @@ -576,7 +449,24 @@ describe('LangChain integration', () => { test('does not truncate input messages when enableTruncation is false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat with full (untruncated) input messages + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]), + ); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs index 3d911666a7d7..7e41037dc2d6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument-with-pii.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs index 05985d888de9..6c76a56deccb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/instrument.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index 032e33c75dfd..309d165fd674 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, @@ -29,141 +28,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - chat model with claude-3-5-sonnet - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second span - chat model with claude-3-opus - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-opus-20240229', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.95, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 200, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - }), - description: 'chat claude-3-opus-20240229', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third span - error handling - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - chat model with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response when recordOutputs: true - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second span - chat model with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-opus-20240229', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.9, - [GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.95, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 200, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response when recordOutputs: true - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: expect.any(String), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }), - description: 'chat claude-3-opus-20240229', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third span - error handling with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'internal_error', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', @@ -172,7 +36,52 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { test('creates langchain related spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat model with claude-3-5-sonnet + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + + // [1] chat model with claude-3-opus + expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [2] error handling + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + }, + }) .start() .completed(); }); @@ -194,7 +103,52 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { test('creates langchain related spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] chat model with PII + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [1] chat model with PII + expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + // [2] error handling with PII + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + }, + }) .start() .completed(); }); @@ -208,34 +162,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { }, ); - const EXPECTED_TRANSACTION_TOOL_CALLS = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 50, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: 'tool_use', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-tools.mjs', @@ -244,7 +170,28 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { test('creates langchain spans with tool calls', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_TOOL_CALLS }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat with tool_use stop reason + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(150); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(50); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('tool_use'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); + }, + }) .start() .completed(); }); @@ -258,68 +205,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { }, ); - const EXPECTED_TRANSACTION_MESSAGE_TRUNCATION = { - transaction: 'main', - spans: expect.arrayContaining([ - // First call: String input truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - // Messages should be present and should include truncated string input (contains only Cs) - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second call: Array input, last message truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.stringMatching(/^\[\{"type":"text","content":"A+"\}\]$/), - // Messages should be present (truncation happened) and should be a JSON array of a single index (contains only Cs) - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third call: Last message is small and kept without truncation - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'claude-3-5-sonnet-20241022', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.stringMatching(/^\[\{"type":"text","content":"A+"\}\]$/), - - // Small message should be kept intact - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]), - }), - description: 'chat claude-3-5-sonnet-20241022', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-message-truncation.mjs', @@ -328,7 +213,40 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { test('truncates messages when they exceed byte limit', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_MESSAGE_TRUNCATION }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] String input truncated (only C's remain, D's are cropped) + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","content":"C+"\}\]$/, + ); + + // [1] Array input, last message truncated (only C's remain, D's are cropped) + expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","content":"C+"\}\]$/, + ); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + /^\[\{"type":"text","content":"A+"\}\]$/, + ); + + // [2] Last message is small and kept without truncation + expect(thirdSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), + ); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + /^\[\{"type":"text","content":"A+"\}\]$/, + ); + }, + }) .start() .completed(); }); @@ -350,46 +268,23 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { test('demonstrates timing issue with duplicate spans (ESM only)', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: event => { - // This test highlights the limitation: if a user creates an Anthropic client - // before importing LangChain, that client will still be instrumented and - // could cause duplicate spans when used alongside LangChain. + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; - const spans = event.spans || []; + // [0] Direct Anthropic call made BEFORE LangChain import — instrumented + // by Anthropic (origin: 'auto.ai.anthropic'). + expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - // First call: Direct Anthropic call made BEFORE LangChain import - // This should have Anthropic instrumentation (origin: 'auto.ai.anthropic') - const firstAnthropicSpan = spans.find( - span => span.description === 'chat claude-3-5-sonnet-20241022' && span.origin === 'auto.ai.anthropic', - ); - - // Second call: LangChain call - // This should have LangChain instrumentation (origin: 'auto.ai.langchain') - const langchainSpan = spans.find( - span => span.description === 'chat claude-3-5-sonnet-20241022' && span.origin === 'auto.ai.langchain', - ); + // [1] LangChain call — instrumented by LangChain (origin: 'auto.ai.langchain'). + expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - // Third call: Direct Anthropic call made AFTER LangChain import - // This should NOT have Anthropic instrumentation (skip works correctly) - // Count how many Anthropic spans we have - should be exactly 1 - const anthropicSpans = spans.filter( - span => span.description === 'chat claude-3-5-sonnet-20241022' && span.origin === 'auto.ai.anthropic', - ); - - // Verify the edge case limitation: - // - First Anthropic client (created before LangChain) IS instrumented - expect(firstAnthropicSpan).toBeDefined(); - expect(firstAnthropicSpan?.origin).toBe('auto.ai.anthropic'); - - // - LangChain call IS instrumented by LangChain - expect(langchainSpan).toBeDefined(); - expect(langchainSpan?.origin).toBe('auto.ai.langchain'); - - // - Second Anthropic client (created after LangChain) is NOT instrumented - // This demonstrates that the skip mechanism works for NEW clients - // We should only have ONE Anthropic span (the first one), not two - expect(anthropicSpans).toHaveLength(1); + // Third call (not present): Direct Anthropic call made AFTER LangChain import + // is NOT instrumented, demonstrating the skip mechanism works for NEW clients. }, }) .start() @@ -406,69 +301,6 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { }, ); - const EXPECTED_TRANSACTION_INIT_CHAT_MODEL = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - initChatModel with gpt-4o - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4o', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 100, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4o', - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: 'stop', - }), - description: 'chat gpt-4o', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Second span - initChatModel with gpt-3.5-turbo - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.5, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]: 'stop', - }), - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'ok', - }), - // Third span - error handling - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langchain', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }), - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.langchain', - status: 'internal_error', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-init-chat-model.mjs', @@ -477,7 +309,52 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { test('creates langchain spans using initChatModel with OpenAI', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_INIT_CHAT_MODEL }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] initChatModel with gpt-4o + expect(firstSpan!.name).toBe('chat gpt-4o'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gpt-4o'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4o'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + + // [1] initChatModel with gpt-3.5-turbo + expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gpt-3.5-turbo'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.5); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-3.5-turbo'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + + // [2] error handling + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs index 91b4e4b1bae5..e91af61469c0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-no-truncation.mjs @@ -14,4 +14,5 @@ Sentry.init({ enableTruncation: false, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs index 2d8d986a2cd1..5bdbaf894ca4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming-with-truncation.mjs @@ -13,4 +13,5 @@ Sentry.init({ enableTruncation: true, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs index 48a860c510c5..7db8f66f7c58 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-streaming.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, traceLifecycle: 'stream', + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs index be512ed2f773..9adaa1c11f4a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument-with-pii.mjs @@ -7,4 +7,5 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: true, transport: loggingTransport, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs index 06cc1a32e93e..69ab61714874 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/instrument.mjs @@ -7,4 +7,5 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: false, transport: loggingTransport, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index d17e789d73f9..95b66e9524f7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -8,7 +8,6 @@ import { GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_PIPELINE_NAME_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, - GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, @@ -25,185 +24,43 @@ describe('LangGraph integration', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'langgraph-test', - spans: expect.arrayContaining([ - // create_agent span - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - }, - description: 'create_agent weather_assistant', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // First invoke_agent span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'weather_assistant', - }), - description: 'invoke_agent weather_assistant', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // Second invoke_agent span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'weather_assistant', - }), - description: 'invoke_agent weather_assistant', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'langgraph-test', - spans: expect.arrayContaining([ - // create_agent span (PII enabled doesn't affect this span) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - }, - description: 'create_agent weather_assistant', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // First invoke_agent span with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('What is the weather today?'), - }), - description: 'invoke_agent weather_assistant', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // Second invoke_agent span with PII and multiple messages - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'weather_assistant', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('Tell me about the weather'), - }), - description: 'invoke_agent weather_assistant', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - ]), - }; - - const EXPECTED_TRANSACTION_WITH_TOOLS = { - transaction: 'langgraph-tools-test', - spans: expect.arrayContaining([ - // create_agent span for first graph (no tool calls) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'tool_agent', - }, - description: 'create_agent tool_agent', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // invoke_agent span with tools available but not called - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'tool_agent', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'tool_agent', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: expect.stringContaining('get_weather'), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('What is the weather?'), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4-0613', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.stringContaining('Response without calling tools'), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - }), - description: 'invoke_agent tool_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // create_agent span for second graph (with tool calls) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'tool_calling_agent', - }, - description: 'create_agent tool_calling_agent', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // invoke_agent span with tool calls and execution - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'tool_calling_agent', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'tool_calling_agent', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: expect.stringContaining('get_weather'), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('San Francisco'), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4-0613', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.stringMatching(/"role":"tool"/), - // Verify tool_calls are captured - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.stringContaining('get_weather'), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 80, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 120, - }), - description: 'invoke_agent tool_calling_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('should instrument LangGraph with default PII settings', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ transaction: { transaction: 'langgraph-test' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] create_agent + expect(firstSpan!.name).toBe('create_agent weather_assistant'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('create_agent'); + expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + + // [1] first invoke_agent + expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); + expect(secondSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + expect(secondSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + + // [2] second invoke_agent + expect(thirdSpan!.name).toBe('invoke_agent weather_assistant'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + expect(thirdSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + }, + }) .start() .completed(); }); @@ -213,7 +70,33 @@ describe('LangGraph integration', () => { test('should instrument LangGraph with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ transaction: { transaction: 'langgraph-test' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] create_agent + expect(firstSpan!.name).toBe('create_agent weather_assistant'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + + // [1] first invoke_agent with PII ("What is the weather today?") + expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'What is the weather today?', + ); + + // [2] second invoke_agent with PII ("Tell me about the weather") + expect(thirdSpan!.name).toBe('invoke_agent weather_assistant'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('Tell me about the weather'); + }, + }) .start() .completed(); }); @@ -221,75 +104,93 @@ describe('LangGraph integration', () => { createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('should capture tools from LangGraph agent', { timeout: 30000 }, async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_WITH_TOOLS }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'langgraph-tools-test' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] create_agent tool_agent + expect(firstSpan!.name).toBe('create_agent tool_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('tool_agent'); + + // [1] invoke_agent tool_agent (tools available, not called) + expect(secondSpan!.name).toBe('invoke_agent tool_agent'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toContain('get_weather'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('What is the weather?'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4-0613'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toContain( + 'Response without calling tools', + ); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(25); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(40); + + // [2] create_agent tool_calling_agent + expect(thirdSpan!.name).toBe('create_agent tool_calling_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(thirdSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('tool_calling_agent'); + + // [3] invoke_agent tool_calling_agent (with tool calls) + expect(fourthSpan!.name).toBe('invoke_agent tool_calling_agent'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('San Francisco'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4-0613'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toMatch(/"role":"tool"/); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toContain('get_weather'); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(80); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(40); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(120); + }, + }) + .start() + .completed(); }); }); // Test for thread_id (conversation ID) support - const EXPECTED_TRANSACTION_THREAD_ID = { - transaction: 'langgraph-thread-id-test', - spans: expect.arrayContaining([ - // create_agent span - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'thread_test_agent', - }, - description: 'create_agent thread_test_agent', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // First invoke_agent span with thread_id - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'thread_test_agent', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'thread_test_agent', - // The thread_id should be captured as conversation.id - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'thread_abc123_session_1', - }), - description: 'invoke_agent thread_test_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // Second invoke_agent span with different thread_id - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'thread_test_agent', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'thread_test_agent', - // Different thread_id for different conversation - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'thread_xyz789_session_2', - }), - description: 'invoke_agent thread_test_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // Third invoke_agent span without thread_id (should NOT have gen_ai.conversation.id) - expect.objectContaining({ - data: expect.not.objectContaining({ - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: expect.anything(), - }), - description: 'invoke_agent thread_test_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-thread-id.mjs', 'instrument.mjs', (createRunner, test) => { test('should capture thread_id as gen_ai.conversation.id', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_THREAD_ID }).start().completed(); + await createRunner() + .ignore('event') + .expect({ transaction: { transaction: 'langgraph-thread-id-test' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] create_agent + expect(firstSpan!.name).toBe('create_agent thread_test_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + + // [1] first invoke_agent with thread_abc123_session_1 + expect(secondSpan!.name).toBe('invoke_agent thread_test_agent'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE].value).toBe('thread_abc123_session_1'); + + // [2] second invoke_agent with thread_xyz789_session_2 + expect(thirdSpan!.name).toBe('invoke_agent thread_test_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE].value).toBe('thread_xyz789_session_2'); + + // [3] third invoke_agent without thread_id + expect(fourthSpan!.name).toBe('invoke_agent thread_test_agent'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); + }, + }) + .start() + .completed(); }); }); @@ -301,18 +202,17 @@ describe('LangGraph integration', () => { test('extracts system instructions from messages', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant' }, - ]), - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(2); + const [, secondSpan] = container.items; + + // [1] invoke_agent with system instructions + expect(secondSpan!.name).toBe('invoke_agent test-agent'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( + JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), + ); }, }) .start() @@ -322,69 +222,48 @@ describe('LangGraph integration', () => { ); // Test for null input resume scenario - const EXPECTED_TRANSACTION_RESUME = { - transaction: 'langgraph-resume-test', - contexts: { - trace: expect.objectContaining({ - status: 'ok', - }), - }, - spans: expect.arrayContaining([ - // create_agent span - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.create_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'resume_agent', - }, - description: 'create_agent resume_agent', - op: 'gen_ai.create_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - // invoke_agent span with null input (resume) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.langgraph', - [GEN_AI_AGENT_NAME_ATTRIBUTE]: 'resume_agent', - [GEN_AI_PIPELINE_NAME_ATTRIBUTE]: 'resume_agent', - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'resume-thread-1', - }), - description: 'invoke_agent resume_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.ai.langgraph', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-resume.mjs', 'instrument.mjs', (createRunner, test) => { test('should not throw when invoke is called with null input (resume scenario)', async () => { - await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_RESUME }).start().completed(); + await createRunner() + .ignore('event') + .expect({ + transaction: { + transaction: 'langgraph-resume-test', + contexts: { + trace: expect.objectContaining({ + status: 'ok', + }), + }, + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan] = container.items; + + // [0] create_agent resume_agent + expect(firstSpan!.name).toBe('create_agent resume_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('resume_agent'); + + // [1] first invoke_agent with thread_id 'resume-thread-1' + expect(secondSpan!.name).toBe('invoke_agent resume_agent'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(secondSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('resume_agent'); + expect(secondSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('resume_agent'); + expect(secondSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE].value).toBe('resume-thread-1'); + }, + }) + .start() + .completed(); }); }); const longContent = 'A'.repeat(50_000); - const EXPECTED_TRANSACTION_NO_TRUNCATION = { - transaction: 'langgraph-test', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-no-truncation.mjs', @@ -393,7 +272,24 @@ describe('LangGraph integration', () => { test('does not truncate input messages when enableTruncation is false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .expect({ transaction: { transaction: 'langgraph-test' } }) + .expect({ + span: container => { + const expectedMessages = JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]); + + expect(container.items).toHaveLength(2); + const [, secondSpan] = container.items; + + // [1] invoke_agent with untruncated input + expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedMessages); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs index 0dd039762f1f..a8c83a29bcfd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-no-truncation.mjs @@ -20,4 +20,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs index f3fbac9d1274..4fad2b24a6d6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-root-span.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: false, transport: loggingTransport, integrations: [Sentry.openAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs index 097c7adcf087..6aef96c5dcc9 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming-with-truncation.mjs @@ -13,4 +13,5 @@ Sentry.init({ enableTruncation: true, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs index 48a860c510c5..7db8f66f7c58 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-streaming.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, traceLifecycle: 'stream', + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs index 86219de9983a..1fd24cc3fa8f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs @@ -19,4 +19,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs index 74bc63db971b..00a67de0ee35 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs index 1ff3990a0693..ac81315e85e4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs index a53a13af7738..6826a894f085 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument-with-pii.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, integrations: [Sentry.openAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs index f3fbac9d1274..4fad2b24a6d6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/instrument.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: false, transport: loggingTransport, integrations: [Sentry.openAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts index c66f5cb65c6b..a300a0a79192 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts @@ -76,212 +76,158 @@ describe('OpenAI Tool Calls integration', () => { }, ]); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - chat completion with tools (non-streaming) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-tools-123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["tool_calls"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - chat completion with tools and streaming - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-stream-tools-123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["tool_calls"]', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - responses API with tools (non-streaming) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_tools_789', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["completed"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fourth span - responses API with tools and streaming - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_stream_tools_789', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["in_progress","completed"]', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - chat completion with tools (non-streaming) with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather like in Paris today?"}]', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-tools-123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["tool_calls"]', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: '[""]', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: CHAT_TOOL_CALLS, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - chat completion with tools and streaming with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather like in Paris today?"}]', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-stream-tools-123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["tool_calls"]', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: CHAT_STREAM_TOOL_CALLS, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - responses API with tools (non-streaming) with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather like in Paris today?"}]', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_tools_789', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["completed"]', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: RESPONSES_TOOL_CALLS, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fourth span - responses API with tools and streaming with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather like in Paris today?"}]', - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: WEATHER_TOOL_DEFINITION, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_stream_tools_789', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["in_progress","completed"]', - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: RESPONSES_TOOL_CALLS, - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates openai tool calls related spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] chat completion with tools (non-streaming) + expect(firstSpan!.name).toBe('chat gpt-4'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-tools-123', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["tool_calls"]', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + + // [1] chat completion with tools and streaming + expect(secondSpan!.name).toBe('chat gpt-4'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-stream-tools-123', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["tool_calls"]', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + + // [2] responses API with tools (non-streaming) + expect(thirdSpan!.name).toBe('chat gpt-4'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_tools_789', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["completed"]', + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + + // [3] responses API with tools and streaming + expect(fourthSpan!.name).toBe('chat gpt-4'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_stream_tools_789', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["in_progress","completed"]', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + }, + }) .start() .completed(); }); @@ -291,7 +237,203 @@ describe('OpenAI Tool Calls integration', () => { test('creates openai tool calls related spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] chat completion with tools (non-streaming) with PII + expect(firstSpan!.name).toBe('chat gpt-4'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-tools-123', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["tool_calls"]', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: '[""]' }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: CHAT_TOOL_CALLS, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + + // [1] chat completion with tools and streaming with PII + expect(secondSpan!.name).toBe('chat gpt-4'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', + }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-stream-tools-123', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["tool_calls"]', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: CHAT_STREAM_TOOL_CALLS, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + + // [2] responses API with tools (non-streaming) with PII + expect(thirdSpan!.name).toBe('chat gpt-4'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', + }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_tools_789', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["completed"]', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: RESPONSES_TOOL_CALLS, + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + + // [3] responses API with tools and streaming with PII + expect(fourthSpan!.name).toBe('chat gpt-4'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: WEATHER_TOOL_DEFINITION, + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_stream_tools_789', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["in_progress","completed"]', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + type: 'string', + value: RESPONSES_TOOL_CALLS, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index e3ecc4f80ae0..3e4de45d3a81 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -29,297 +29,193 @@ describe('OpenAI integration', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_CHAT = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - basic chat completion without PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-mock123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - responses API - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_mock456', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["completed"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 5, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 13, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - error handling - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - // Fourth span - chat completions streaming - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8, - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-stream-123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 18, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fifth span - responses API streaming - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_stream_456', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["in_progress","completed"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 6, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 16, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Sixth span - error handling in streaming context - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_CHAT = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - basic chat completion with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the capital of France?"}]', - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant.' }, - ]), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-mock123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: '["Hello from OpenAI mock!"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - responses API with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: 'Translate this to French: Hello', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Response to: Translate this to French: Hello', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["completed"]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_mock456', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 5, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 13, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - error handling with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"This will fail"}]', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - // Fourth span - chat completions streaming with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8, - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Tell me about streaming"}]', - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant.' }, - ]), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from OpenAI streaming!', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-stream-123', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 18, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fifth span - responses API streaming with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: 'Test streaming responses API', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: - 'Streaming response to: Test streaming responses APITest streaming responses API', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["in_progress","completed"]', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_stream_456', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 6, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 16, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Sixth span - error handling in streaming context with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"This will fail"}]', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_WITH_OPTIONS = { - transaction: 'main', - spans: expect.arrayContaining([ - // Check that custom options are respected - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text when recordOutputs: true - }), - }), - // Check that custom options are respected for streaming - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text when recordOutputs: true - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, // Should be marked as stream - }), - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-chat.mjs', 'instrument.mjs', (createRunner, test) => { test('creates openai related spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_CHAT }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(6); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; + + // [0] basic chat completion without PII + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-mock123', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + + // [1] responses API + expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_mock456', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["completed"]', + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 5 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 13 }); + + // [2] error handling (non-streaming) + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + + // [3] chat completions streaming + expect(fourthSpan!.name).toBe('chat gpt-4'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.8, + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-stream-123', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 18, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30 }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [4] responses API streaming + expect(fifthSpan!.name).toBe('chat gpt-4'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_stream_456', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["in_progress","completed"]', + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 6 }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 16 }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [5] error handling in streaming context + expect(sixthSpan!.name).toBe('chat error-model'); + expect(sixthSpan!.status).toBe('error'); + expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + }, + }) .start() .completed(); }); @@ -329,7 +225,261 @@ describe('OpenAI integration', () => { test('creates openai related spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_CHAT }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(6); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; + + // [0] basic chat completion with PII + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the capital of France?"}]', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: JSON.stringify([{ type: 'text', content: 'You are a helpful assistant.' }]), + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-mock123', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["Hello from OpenAI mock!"]', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + + // [1] responses API with PII + expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Translate this to French: Hello', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Response to: Translate this to French: Hello', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["completed"]', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_mock456', + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 5 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 13 }); + + // [2] error handling with PII (non-streaming) + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"This will fail"}]', + }); + + // [3] chat completions streaming with PII + expect(fourthSpan!.name).toBe('chat gpt-4'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.8, + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"Tell me about streaming"}]', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: JSON.stringify([{ type: 'text', content: 'You are a helpful assistant.' }]), + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Hello from OpenAI streaming!', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-stream-123', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 18, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30 }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [4] responses API streaming with PII + expect(fifthSpan!.name).toBe('chat gpt-4'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Test streaming responses API', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Streaming response to: Test streaming responses APITest streaming responses API', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["in_progress","completed"]', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_stream_456', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 6 }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 16 }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [5] error handling in streaming context with PII + expect(sixthSpan!.name).toBe('chat error-model'); + expect(sixthSpan!.status).toBe('error'); + expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"This will fail"}]', + }); + expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + }, + }) .start() .completed(); }); @@ -339,7 +489,35 @@ describe('OpenAI integration', () => { test('creates openai related spans with custom options', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(6); + const [firstSpan, , , fourthSpan] = container.items; + + // [0] non-streaming with input messages recorded via custom options + expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + + // [3] streaming with input messages recorded via custom options + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + }, + }) .start() .completed(); }); @@ -347,30 +525,6 @@ describe('OpenAI integration', () => { const longContent = 'A'.repeat(50_000); - const EXPECTED_TRANSACTION_NO_TRUNCATION = { - transaction: 'main', - spans: expect.arrayContaining([ - // Multiple messages should all be preserved (no popping to last message only) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - }), - // Responses API long string input should not be truncated or wrapped in quotes - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: 'B'.repeat(50_000), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - }), - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-no-truncation.mjs', @@ -379,115 +533,153 @@ describe('OpenAI integration', () => { test('does not truncate input messages when enableTruncation is false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_NO_TRUNCATION }) + .expect({ + transaction: { + transaction: 'main', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] chat completions: multiple messages all preserved (no popping to last message only) + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-mock123', + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]), + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + type: 'integer', + value: 3, + }); + + // [1] responses API long string input is not truncated or wrapped in quotes + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_mock456', + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: 'B'.repeat(50_000), + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + type: 'integer', + value: 1, + }); + }, + }) .start() .completed(); }); }, ); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - embeddings API - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]: 'float', - [GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]: 1536, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }, - description: 'embeddings text-embedding-3-small', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - embeddings API error model - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }, - description: 'embeddings error-model', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - embeddings API with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]: 'float', - [GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]: 1536, - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'Embedding test!', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }, - description: 'embeddings text-embedding-3-small', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - embeddings API error model with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'Error embedding test!', - }, - description: 'embeddings error-model', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - // Third span - embeddings API with multiple inputs (this does not get truncated) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: '["First input text","Second input text","Third input text"]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }, - description: 'embeddings text-embedding-3-small', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates openai related spans with sendDefaultPii: false', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS }) + .expect({ + transaction: { + transaction: 'main', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embeddings API (single input) + expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'float', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1536, + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + + // [1] embeddings API error model + expect(secondSpan!.name).toBe('embeddings error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + + // [2] embeddings API (multiple inputs) + expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + }, + }) .start() .completed(); }); @@ -497,7 +689,112 @@ describe('OpenAI integration', () => { test('creates openai related spans with sendDefaultPii: true', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS }) + .expect({ + transaction: { + transaction: 'main', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embeddings API with PII (single input) + expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'float', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1536, + }); + expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Embedding test!', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + + // [1] embeddings API error model with PII + expect(secondSpan!.name).toBe('embeddings error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Error embedding test!', + }); + + // [2] embeddings API with multiple inputs (not truncated) + expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["First input text","Second input text","Third input text"]', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + }, + }) .start() .completed(); }); @@ -598,50 +895,74 @@ describe('OpenAI integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - // First call: Last message is large and gets truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - // Messages should be present (truncation happened) and should be a JSON array of a single index - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.stringMatching( - /^\[\{"type":"text","content":"A+"\}\]$/, - ), - }), - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second call: Last message is small and kept without truncation - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - // Small message should be kept intact - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: 'This is a small message that fits within the limit' }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 2, - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.stringMatching( - /^\[\{"type":"text","content":"A+"\}\]$/, - ), - }), - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] Last message is large and gets truncated (only C's remain, D's are cropped) + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 2, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[\{"role":"user","content":"C+"\}\]$/, + ); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + /^\[\{"type":"text","content":"A+"\}\]$/, + ); + + // [1] Last message is small and kept without truncation + expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: JSON.stringify([ + { role: 'user', content: 'This is a small message that fits within the limit' }, + ]), + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 2, + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + /^\[\{"type":"text","content":"A+"\}\]$/, + ); }, }) .start() @@ -661,24 +982,35 @@ describe('OpenAI integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - // Messages should be present and should include truncated string input (contains only As) - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^A+$/), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - }), - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] long A-string input is truncated + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch(/^A+$/); }, }) .start() @@ -688,201 +1020,151 @@ describe('OpenAI integration', () => { ); // Test for conversation ID support (Conversations API and previous_response_id) - const EXPECTED_TRANSACTION_CONVERSATION = { - transaction: 'conversation-test', - spans: expect.arrayContaining([ - // First span - conversations.create returns conversation object with id - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - // The conversation ID should be captured from the response - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'conv_689667905b048191b4740501625afd940c7533ace33a2dab', - }), - description: 'chat unknown', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - responses.create with conversation parameter - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - // The conversation ID should be captured from the request - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'conv_689667905b048191b4740501625afd940c7533ace33a2dab', - }), - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - responses.create without conversation (first in chain, should NOT have gen_ai.conversation.id) - expect.objectContaining({ - data: expect.not.objectContaining({ - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: expect.anything(), - }), - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fourth span - responses.create with previous_response_id (chaining) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - // The previous_response_id should be captured as conversation.id - [GEN_AI_CONVERSATION_ID_ATTRIBUTE]: 'resp_mock_conv_123', - }), - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-conversation.mjs', 'instrument.mjs', (createRunner, test) => { test('captures conversation ID from Conversations API and previous_response_id', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_CONVERSATION }) + .expect({ + transaction: { + transaction: 'conversation-test', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] conversations.create returns conversation object with id + expect(firstSpan!.name).toBe('chat unknown'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'conv_689667905b048191b4740501625afd940c7533ace33a2dab', + }); + + // [1] responses.create with conversation parameter + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(secondSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'conv_689667905b048191b4740501625afd940c7533ace33a2dab', + }); + + // [2] responses.create without conversation (first in chain, should NOT have gen_ai.conversation.id) + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); + + // [3] responses.create with previous_response_id (chaining) + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fourthSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_mock_conv_123', + }); + }, + }) .start() .completed(); }); }); // Test for manual conversation ID setting using setConversationId() - const EXPECTED_TRANSACTION_MANUAL_CONVERSATION_ID = { - transaction: 'chat-with-manual-conversation-id', - spans: expect.arrayContaining([ - // All three chat completion spans should have the same manually-set conversation ID - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'user_chat_session_abc123', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'gen_ai.operation.name': 'chat', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'user_chat_session_abc123', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'gen_ai.operation.name': 'chat', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'user_chat_session_abc123', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'gen_ai.operation.name': 'chat', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-manual-conversation-id.mjs', 'instrument.mjs', (createRunner, test) => { test('attaches manual conversation ID set via setConversationId() to all chat spans', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_MANUAL_CONVERSATION_ID }) + .expect({ + transaction: { + transaction: 'chat-with-manual-conversation-id', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // All three chat completion spans should have the same manually-set conversation ID + for (const span of [firstSpan, secondSpan, thirdSpan]) { + expect(span!.name).toBe('chat gpt-4'); + expect(span!.status).toBe('ok'); + expect(span!.attributes['gen_ai.conversation.id']).toEqual({ + type: 'string', + value: 'user_chat_session_abc123', + }); + expect(span!.attributes['gen_ai.system']).toEqual({ type: 'string', value: 'openai' }); + expect(span!.attributes['gen_ai.request.model']).toEqual({ type: 'string', value: 'gpt-4' }); + expect(span!.attributes['gen_ai.operation.name']).toEqual({ type: 'string', value: 'chat' }); + expect(span!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + } + }, + }) .start() .completed(); }); }); - // Test for scope isolation - different scopes have different conversation IDs - const EXPECTED_TRANSACTION_CONVERSATION_1 = { - transaction: 'GET /chat/conversation-1', - spans: expect.arrayContaining([ - // Both chat completion spans in conversation 1 should have conv_user1_session_abc - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'conv_user1_session_abc', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'conv_user1_session_abc', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - - const EXPECTED_TRANSACTION_CONVERSATION_2 = { - transaction: 'GET /chat/conversation-2', - spans: expect.arrayContaining([ - // Both chat completion spans in conversation 2 should have conv_user2_session_xyz - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'conv_user2_session_xyz', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'conv_user2_session_xyz', - 'gen_ai.system': 'openai', - 'gen_ai.request.model': 'gpt-4', - 'sentry.op': 'gen_ai.chat', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-separate-scope-1.mjs', 'instrument.mjs', (createRunner, test) => { test('isolates conversation IDs across separate scopes - conversation 1', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_CONVERSATION_1 }) + .expect({ + transaction: { + transaction: 'GET /chat/conversation-1', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // Both chat completion spans should have the expected conversation ID + for (const span of [firstSpan, secondSpan]) { + expect(span!.name).toBe('chat gpt-4'); + expect(span!.status).toBe('ok'); + expect(span!.attributes['gen_ai.conversation.id']).toEqual({ + type: 'string', + value: 'conv_user1_session_abc', + }); + expect(span!.attributes['gen_ai.system']).toEqual({ type: 'string', value: 'openai' }); + expect(span!.attributes['gen_ai.request.model']).toEqual({ type: 'string', value: 'gpt-4' }); + expect(span!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + } + }, + }) .start() .completed(); }); @@ -892,7 +1174,30 @@ describe('OpenAI integration', () => { test('isolates conversation IDs across separate scopes - conversation 2', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_CONVERSATION_2 }) + .expect({ + transaction: { + transaction: 'GET /chat/conversation-2', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // Both chat completion spans should have the expected conversation ID + for (const span of [firstSpan, secondSpan]) { + expect(span!.name).toBe('chat gpt-4'); + expect(span!.status).toBe('ok'); + expect(span!.attributes['gen_ai.conversation.id']).toEqual({ + type: 'string', + value: 'conv_user2_session_xyz', + }); + expect(span!.attributes['gen_ai.system']).toEqual({ type: 'string', value: 'openai' }); + expect(span!.attributes['gen_ai.request.model']).toEqual({ type: 'string', value: 'gpt-4' }); + expect(span!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + } + }, + }) .start() .completed(); }); @@ -909,15 +1214,18 @@ describe('OpenAI integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant' }, - ]), - }), - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(1); + const [firstSpan] = container.items; + + // [0] chat completion with system instructions extracted from messages + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), + }); }, }) .start() @@ -933,30 +1241,24 @@ describe('OpenAI integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - // First call using .withResponse() - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-withresponse', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - status: 'ok', - }), - // Second call using .asResponse() - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-withresponse', - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - status: 'ok', - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // Both calls should produce spans with the same response ID + for (const span of [firstSpan, secondSpan]) { + expect(span!.name).toBe('chat gpt-4'); + expect(span!.status).toBe('ok'); + expect(span!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(span!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(span!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-withresponse', + }); + } }, }) .start() @@ -971,32 +1273,25 @@ describe('OpenAI integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - // Single image vision request - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4o', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('[Blob substitute]'), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - }), - description: 'chat gpt-4o', - op: 'gen_ai.chat', - status: 'ok', - }), - // Multiple images vision request - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4o', - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('[Blob substitute]'), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - }), - description: 'chat gpt-4o', - op: 'gen_ai.chat', - status: 'ok', - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // Both vision request spans should contain [Blob substitute] + for (const span of [firstSpan, secondSpan]) { + expect(span!.name).toBe('chat gpt-4o'); + expect(span!.status).toBe('ok'); + expect(span!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(span!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4o' }); + expect(span!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(span!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('[Blob substitute]'); + } }, }) .start() @@ -1009,14 +1304,17 @@ describe('OpenAI integration', () => { .expect({ transaction: { transaction: 'main', - spans: expect.arrayContaining([ - // The second span (multiple images) should still contain the https URL - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining('https://example.com/image.png'), - }), - }), - ]), + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [, secondSpan] = container.items; + + // [1] multiple images span contains the https URL + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'https://example.com/image.png', + ); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs index f3fbac9d1274..4fad2b24a6d6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-root-span.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: false, transport: loggingTransport, integrations: [Sentry.openAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs index 51da27f73bbc..4632dcada76a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-options.mjs @@ -20,4 +20,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs index 74bc63db971b..00a67de0ee35 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument-with-pii.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs index 1ff3990a0693..ac81315e85e4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/instrument.mjs @@ -14,4 +14,5 @@ Sentry.init({ } return event; }, + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index b282282305eb..906cb5ee61c4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -28,390 +28,6 @@ describe('OpenAI integration (V6)', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_CHAT = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - basic chat completion without PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-mock123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - responses API - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_mock456', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["completed"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 5, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 13, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - error handling - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - // Fourth span - chat completions streaming - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8, - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-stream-123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 18, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fifth span - responses API streaming - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_stream_456', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["in_progress","completed"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 6, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 16, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }, - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Sixth span - error handling in streaming context - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_CHAT = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - basic chat completion with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.7, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the capital of France?"}]', - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: '[{"type":"text","content":"You are a helpful assistant."}]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-mock123', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: '["Hello from OpenAI mock!"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 25, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - responses API with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: 'Translate this to French: Hello', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Response to: Translate this to French: Hello', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["completed"]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-3.5-turbo', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_mock456', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 5, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 13, - }, - description: 'chat gpt-3.5-turbo', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Third span - error handling with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"This will fail"}]', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - // Fourth span - chat completions streaming with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8, - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Tell me about streaming"}]', - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: '[{"type":"text","content":"You are a helpful assistant."}]', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Hello from OpenAI streaming!', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["stop"]', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-stream-123', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 12, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 18, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Fifth span - responses API streaming with PII - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: 'Test streaming responses API', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: - 'Streaming response to: Test streaming responses APITest streaming responses API', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: '["in_progress","completed"]', - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'resp_stream_456', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'gpt-4', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 6, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 16, - [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true, - }), - description: 'chat gpt-4', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Sixth span - error handling in streaming context with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"This will fail"}]', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - }, - description: 'chat error-model', - op: 'gen_ai.chat', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_WITH_OPTIONS = { - transaction: 'main', - spans: expect.arrayContaining([ - // Check that custom options are respected - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.any(String), // System instructions should be extracted - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text when recordOutputs: true - }), - }), - // Check that custom options are respected for streaming - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), // Should include messages when recordInputs: true - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: expect.any(String), // System instructions should be extracted - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), // Should include response text when recordOutputs: true - [GEN_AI_REQUEST_STREAM_ATTRIBUTE]: true, // Should be marked as stream - }), - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - embeddings API - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]: 'float', - [GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]: 1536, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }, - description: 'embeddings text-embedding-3-small', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - embeddings API error model - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - }, - description: 'embeddings error-model', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - ]), - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - embeddings API with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]: 'float', - [GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]: 1536, - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'Embedding test!', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }, - description: 'embeddings text-embedding-3-small', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'ok', - }), - // Second span - embeddings API error model with PII - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'Error embedding test!', - }, - description: 'embeddings error-model', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'internal_error', - }), - // Third span - embeddings API with multiple inputs (this does not get truncated) - expect.objectContaining({ - data: { - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.openai', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: '["First input text","Second input text","Third input text"]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'text-embedding-3-small', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }, - description: 'embeddings text-embedding-3-small', - op: 'gen_ai.embeddings', - origin: 'auto.ai.openai', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario-chat.mjs', @@ -420,7 +36,240 @@ describe('OpenAI integration (V6)', () => { test('creates openai related spans with sendDefaultPii: false (v6)', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_CHAT }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(6); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; + + // [0] basic chat completion without PII + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.7, + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-mock123', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); + + // [1] responses API + expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_mock456', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["completed"]', + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 5, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 13, + }); + + // [2] error handling (non-streaming) + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + + // [3] chat completions streaming + expect(fourthSpan!.name).toBe('chat gpt-4'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.8, + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-stream-123', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 18, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 30, + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [4] responses API streaming + expect(fifthSpan!.name).toBe('chat gpt-4'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_stream_456', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["in_progress","completed"]', + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 6, + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 16, + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [5] error handling in streaming context + expect(sixthSpan!.name).toBe('chat error-model'); + expect(sixthSpan!.status).toBe('error'); + expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + }, + }) .start() .completed(); }); @@ -440,7 +289,312 @@ describe('OpenAI integration (V6)', () => { test('creates openai related spans with sendDefaultPii: true (v6)', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_CHAT }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(6); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; + + // [0] basic chat completion with PII + expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.7, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"What is the capital of France?"}]', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"type":"text","content":"You are a helpful assistant."}]', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-mock123', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["Hello from OpenAI mock!"]', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); + + // [1] responses API with PII + expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Translate this to French: Hello', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Response to: Translate this to French: Hello', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["completed"]', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-3.5-turbo', + }); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_mock456', + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 5, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 13, + }); + + // [2] error handling with PII (non-streaming) + expect(thirdSpan!.name).toBe('chat error-model'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"This will fail"}]', + }); + + // [3] chat completions streaming with PII + expect(fourthSpan!.name).toBe('chat gpt-4'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.8, + }); + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"Tell me about streaming"}]', + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"type":"text","content":"You are a helpful assistant."}]', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Hello from OpenAI streaming!', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["stop"]', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chatcmpl-stream-123', + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 18, + }); + expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 30, + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [4] responses API streaming with PII + expect(fifthSpan!.name).toBe('chat gpt-4'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); + expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Test streaming responses API', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Streaming response to: Test streaming responses APITest streaming responses API', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["in_progress","completed"]', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'resp_stream_456', + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 6, + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 16, + }); + expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + + // [5] error handling in streaming context with PII + expect(sixthSpan!.name).toBe('chat error-model'); + expect(sixthSpan!.status).toBe('error'); + expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1, + }); + expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[{"role":"user","content":"This will fail"}]', + }); + expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.chat', + }); + expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + }, + }) .start() .completed(); }); @@ -460,7 +614,51 @@ describe('OpenAI integration (V6)', () => { test('creates openai related spans with custom options (v6)', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }) + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(6); + const [firstSpan, , , fourthSpan] = container.items; + + // [0] non-streaming with input messages recorded via custom options + expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + type: 'integer', + value: 1, + }); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + + // [3] streaming with input messages recorded via custom options + expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + type: 'integer', + value: 1, + }); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + expect(fourthSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + type: 'string', + value: expect.any(String), + }); + }, + }) .start() .completed(); }); @@ -480,7 +678,112 @@ describe('OpenAI integration (V6)', () => { test('creates openai related spans with sendDefaultPii: false (v6)', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS }) + .expect({ + transaction: { + transaction: 'main', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embeddings API (single input) + expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'float', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1536, + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + + // [1] embeddings API error model + expect(secondSpan!.name).toBe('embeddings error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + + // [2] embeddings API (multiple inputs) + expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + }, + }) .start() .completed(); }); @@ -500,7 +803,124 @@ describe('OpenAI integration (V6)', () => { test('creates openai related spans with sendDefaultPii: true (v6)', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS }) + .expect({ + transaction: { + transaction: 'main', + }, + }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] embeddings API with PII (single input) + expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'float', + }); + expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 1536, + }); + expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Embedding test!', + }); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + + // [1] embeddings API error model with PII + expect(secondSpan!.name).toBe('embeddings error-model'); + expect(secondSpan!.status).toBe('error'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'error-model', + }); + expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'Error embedding test!', + }); + + // [2] embeddings API with multiple inputs (not truncated) + expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + type: 'string', + value: 'gen_ai.embeddings', + }); + expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + type: 'string', + value: 'auto.ai.openai', + }); + expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + type: 'string', + value: '["First input text","Second input text","Third input text"]', + }); + expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'text-embedding-3-small', + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + }, + }) .start() .completed(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs index 0593d975c8d7..59638403d00f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-no-truncation.mjs @@ -14,4 +14,5 @@ Sentry.init({ enableTruncation: false, }), ], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs index b798e21228f5..ddc247cc2d41 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs index 5e898ee1949d..a76d206a0b61 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs @@ -7,4 +7,5 @@ Sentry.init({ tracesSampleRate: 1.0, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts index 39e13d5425c2..54c64bc2172b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts @@ -6,62 +6,45 @@ describe('Vercel AI integration - generateObject', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION = { - transaction: 'main', - spans: expect.arrayContaining([ - // generateObject span - expect.objectContaining({ - data: expect.objectContaining({ - 'vercel.ai.model.id': 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateObject', - 'vercel.ai.pipeline.name': 'generateObject', - 'vercel.ai.streaming': false, - 'vercel.ai.settings.mode': 'json', - 'vercel.ai.settings.output': 'object', - 'gen_ai.request.schema': expect.any(String), - 'gen_ai.response.model': 'mock-model-id', - 'gen_ai.usage.input_tokens': 15, - 'gen_ai.usage.output_tokens': 25, - 'gen_ai.usage.total_tokens': 40, - 'gen_ai.operation.name': 'invoke_agent', - 'sentry.op': 'gen_ai.invoke_agent', - 'sentry.origin': 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // generateObject.doGenerate span - expect.objectContaining({ - data: expect.objectContaining({ - 'sentry.origin': 'auto.vercelai.otel', - 'sentry.op': 'gen_ai.generate_content', - 'gen_ai.operation.name': 'generate_content', - 'vercel.ai.operationId': 'ai.generateObject.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.model.id': 'mock-model-id', - 'vercel.ai.pipeline.name': 'generateObject.doGenerate', - 'vercel.ai.streaming': false, - 'gen_ai.system': 'mock-provider', - 'gen_ai.request.model': 'mock-model-id', - 'gen_ai.response.model': 'mock-model-id', - 'gen_ai.usage.input_tokens': 15, - 'gen_ai.usage.output_tokens': 25, - 'gen_ai.usage.total_tokens': 40, - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario-generate-object.mjs', 'instrument.mjs', (createRunner, test) => { test('captures generateObject spans with schema attributes', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] generateObject (invoke_agent) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateObject'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); + expect(firstSpan!.attributes['gen_ai.operation.name'].value).toBe('invoke_agent'); + expect(firstSpan!.attributes['gen_ai.response.model'].value).toBe('mock-model-id'); + expect(firstSpan!.attributes['gen_ai.usage.input_tokens'].value).toBe(15); + expect(firstSpan!.attributes['gen_ai.usage.output_tokens'].value).toBe(25); + expect(firstSpan!.attributes['gen_ai.usage.total_tokens'].value).toBe(40); + expect(firstSpan!.attributes['gen_ai.request.schema']).toBeDefined(); + + // [1] generateObject.doGenerate (generate_content) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateObject.doGenerate'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); + expect(secondSpan!.attributes['gen_ai.operation.name'].value).toBe('generate_content'); + expect(secondSpan!.attributes['gen_ai.system'].value).toBe('mock-provider'); + expect(secondSpan!.attributes['gen_ai.request.model'].value).toBe('mock-model-id'); + expect(secondSpan!.attributes['gen_ai.response.model'].value).toBe('mock-model-id'); + expect(secondSpan!.attributes['gen_ai.usage.input_tokens'].value).toBe(15); + expect(secondSpan!.attributes['gen_ai.usage.output_tokens'].value).toBe(25); + expect(secondSpan!.attributes['gen_ai.usage.total_tokens'].value).toBe(40); + }, + }) + .start() + .completed(); }); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index d75a1faf8ea0..e3091415e18d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -1,4 +1,3 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { Event } from '@sentry/node'; import { afterAll, describe, expect } from 'vitest'; import { @@ -9,8 +8,6 @@ import { GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, - GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, @@ -31,576 +28,207 @@ describe('Vercel AI integration', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - no telemetry config, should enable telemetry but not record inputs/outputs when sendDefaultPii: false - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Second span - explicitly enabled telemetry but recordInputs/recordOutputs not set, should not record when sendDefaultPii: false - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Third span - explicit telemetry enabled, should record inputs/outputs regardless of sendDefaultPii - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fourth span - doGenerate for explicit telemetry enabled call - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.prompt.format': expect.any(String), - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fifth span - tool call generateText span - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Sixth span - tool call doGenerate span - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Seventh span - tool call execution span - // Note: gen_ai.tool.description is NOT present when sendDefaultPii: false because ai.prompt.tools is not recorded - expect.objectContaining({ - data: { - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.operationId': 'ai.toolCall', - }, - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - - const EXPECTED_AVAILABLE_TOOLS_JSON = - '[{"type":"function","name":"getWeather","description":"Get the current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]'; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - no telemetry config, should enable telemetry AND record inputs/outputs when sendDefaultPii: true - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the first span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the first span?"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - // Second span - doGenerate for first call, should also include input/output fields when sendDefaultPii: true - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.prompt.format': 'prompt', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - // Third span - explicitly enabled telemetry, should record inputs/outputs regardless of sendDefaultPii - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - // Fourth span - doGenerate for explicitly enabled telemetry call - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.prompt.format': expect.any(String), - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - // Fifth span - tool call generateText span (should include prompts when sendDefaultPii: true) - expect.objectContaining({ - data: { - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in San Francisco?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Tool call completed!"},{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{ \\"location\\": \\"San Francisco\\" }"}],"finish_reason":"tool_call"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"What is the weather in San Francisco?"}]', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - // Sixth span - tool call doGenerate span (should include prompts when sendDefaultPii: true) - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Tool call completed!"},{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{ \\"location\\": \\"San Francisco\\" }"}],"finish_reason":"tool_call"}]', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.prompt.format': expect.any(String), - 'vercel.ai.prompt.toolChoice': expect.any(String), - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - // Seventh span - tool call execution span - expect.objectContaining({ - data: { - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE]: 'Get the current weather for a location', - [GEN_AI_TOOL_INPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_OUTPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.operationId': 'ai.toolCall', - }, - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - }), - ]), - }; - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates ai related spans with sendDefaultPii: false', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; + + // [0] First generateText — invoke_agent (no explicit telemetry, no PII) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + // [1] First generateText — generate_content (doGenerate, no PII) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + // [2] Second generateText — invoke_agent (explicit telemetry enabled) + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(thirdSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', + ); + + // [3] Second generateText — generate_content (doGenerate with telemetry) + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(fourthSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('Second span here!'); + + // [4] Third generateText — invoke_agent (tool call) + expect(fifthSpan!.name).toBe('invoke_agent'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(25); + expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(40); + + // [5] Third generateText — generate_content (doGenerate with tools) + expect(sixthSpan!.name).toBe('generate_content mock-model-id'); + expect(sixthSpan!.status).toBe('ok'); + expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + // [6] Tool execution + expect(seventhSpan!.name).toBe('execute_tool getWeather'); + expect(seventhSpan!.status).toBe('ok'); + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + }, + }) + .start() + .completed(); }); }); createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates ai related spans with sendDefaultPii: true', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; + + // [0] First generateText — invoke_agent (PII auto-enabled) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the first span?"}]', + ); + expect(firstSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', + ); + + // [1] First doGenerate with PII + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('First span here!'); + + // [2] Second generateText — invoke_agent (explicit telemetry) + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the second span?"}]', + ); + + // [3] Second doGenerate + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [4] Third generateText — invoke_agent (tool call prompt) + expect(fifthSpan!.name).toBe('invoke_agent'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"What is the weather in San Francisco?"}]', + ); + expect(fifthSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + + // [5] Third doGenerate with available tools + expect(sixthSpan!.name).toBe('generate_content mock-model-id'); + expect(sixthSpan!.status).toBe('ok'); + expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toContain('getWeather'); + expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + // [6] Tool execution with PII + expect(seventhSpan!.name).toBe('execute_tool getWeather'); + expect(seventhSpan!.status).toBe('ok'); + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( + 'Get the current weather for a location', + ); + expect(seventhSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); + expect(seventhSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); + }, + }) + .start() + .completed(); }); }); createEsmAndCjsTests(__dirname, 'scenario-error-in-tool.mjs', 'instrument.mjs', (createRunner, test) => { test('captures error in tool', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'internal_error', - }), - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - expect.objectContaining({ - data: { - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.operationId': 'ai.toolCall', - }, - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'internal_error', - }), - ]), - - tags: { - 'test-tag': 'test-value', - }, - }; - let traceId: string = 'unset-trace-id'; let spanId: string = 'unset-span-id'; - const expectedError = { - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - }, - }, - exception: { - values: expect.arrayContaining([ - expect.objectContaining({ - type: 'AI_ToolExecutionError', - value: 'Error executing tool getWeather: Error in tool', - }), - ]), - }, - tags: { - 'test-tag': 'test-value', - }, - }; - await createRunner() .expect({ transaction: transaction => { - expect(transaction).toMatchObject(expectedTransaction); + expect(transaction.transaction).toBe('main'); + // gen_ai spans should be empty in transaction + expect(transaction.spans).toEqual([]); traceId = transaction.contexts!.trace!.trace_id; spanId = transaction.contexts!.trace!.span_id; }, }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] invoke_agent (errored due to tool error) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('error'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + + // [1] generate_content (doGenerate, succeeded) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + + // [2] execute_tool (errored) + expect(thirdSpan!.name).toBe('execute_tool getWeather'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + }, + }) .expect({ event: event => { - expect(event).toMatchObject(expectedError); + expect(event.exception?.values).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'AI_ToolExecutionError', + value: 'Error executing tool getWeather: Error in tool', + }), + ]), + ); + expect(event.tags).toMatchObject({ 'test-tag': 'test-value' }); expect(event.contexts!.trace!.trace_id).toBe(traceId); expect(event.contexts!.trace!.span_id).toBe(spanId); }, @@ -612,101 +240,6 @@ describe('Vercel AI integration', () => { createEsmAndCjsTests(__dirname, 'scenario-error-in-tool-express.mjs', 'instrument.mjs', (createRunner, test) => { test('captures error in tool in express server', async () => { - const expectedTransaction = { - transaction: 'GET /test/error-in-tool', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.settings.maxSteps': 1, - 'vercel.ai.streaming': false, - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'internal_error', - }), - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - expect.objectContaining({ - data: { - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.operationId': 'ai.toolCall', - }, - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'internal_error', - }), - ]), - - tags: { - 'test-tag': 'test-value', - }, - }; - - const expectedError = { - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - }, - }, - exception: { - values: expect.arrayContaining([ - expect.objectContaining({ - type: 'AI_ToolExecutionError', - value: 'Error executing tool getWeather: Error in tool', - }), - ]), - }, - tags: { - 'test-tag': 'test-value', - }, - }; - let transactionEvent: Event | undefined; let errorEvent: Event | undefined; @@ -716,6 +249,29 @@ describe('Vercel AI integration', () => { transactionEvent = transaction; }, }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] invoke_agent (errored) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('error'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + + // [1] generate_content (doGenerate, succeeded) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [2] execute_tool (errored) + expect(thirdSpan!.name).toBe('execute_tool getWeather'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + }, + }) .expect({ event: event => { errorEvent = event; @@ -727,11 +283,19 @@ describe('Vercel AI integration', () => { await runner.completed(); expect(transactionEvent).toBeDefined(); - expect(errorEvent).toBeDefined(); - - expect(transactionEvent).toMatchObject(expectedTransaction); + expect(transactionEvent!.transaction).toBe('GET /test/error-in-tool'); + expect(transactionEvent!.tags).toMatchObject({ 'test-tag': 'test-value' }); - expect(errorEvent).toMatchObject(expectedError); + expect(errorEvent).toBeDefined(); + expect(errorEvent!.exception?.values).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'AI_ToolExecutionError', + value: 'Error executing tool getWeather: Error in tool', + }), + ]), + ); + expect(errorEvent!.tags).toMatchObject({ 'test-tag': 'test-value' }); expect(errorEvent!.contexts!.trace!.trace_id).toBe(transactionEvent!.contexts!.trace!.trace_id); expect(errorEvent!.contexts!.trace!.span_id).toBe(transactionEvent!.contexts!.trace!.span_id); }); @@ -739,37 +303,30 @@ describe('Vercel AI integration', () => { createEsmAndCjsTests(__dirname, 'scenario-late-model-id.mjs', 'instrument.mjs', (createRunner, test) => { test('sets op correctly even when model ID is not available at span start', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - // The generateText span should have the correct op even though model ID was not available at span start - expect.objectContaining({ - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - }), - }), - // The doGenerate span - name stays as 'generateText.doGenerate' since model ID is missing - expect.objectContaining({ - description: 'generateText.doGenerate', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - }), - }), - ]), - }; + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] invoke_agent + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); + expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); - await createRunner().expect({ transaction: expectedTransaction }).start().completed(); + // [1] generate_content (doGenerate) + expect(secondSpan!.name).toBe('generateText.doGenerate'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); + expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + }, + }) + .start() + .completed(); }); }); @@ -781,18 +338,22 @@ describe('Vercel AI integration', () => { test('extracts system instructions from messages', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]: JSON.stringify([ - { type: 'text', content: 'You are a helpful assistant' }, - ]), - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] invoke_agent (carries system instructions) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( + JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), + ); + + // [1] generate_content + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); }, }) .start() @@ -809,27 +370,27 @@ describe('Vercel AI integration', () => { test('truncates messages when they exceed byte limit', async () => { await createRunner() .ignore('event') + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - // First call: Last message truncated (only C's remain, D's are cropped) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[.*"(?:text|content)":"C+".*\]$/), - }), - }), - // Second call: Last message is small and kept intact - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining( - 'This is a small message that fits within the limit', - ), - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, , thirdSpan] = container.items; + + // [0] First call — invoke_agent: last message truncated (only C's remain, D's cropped) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + /^\[.*"(?:text|content)":"C+".*\]$/, + ); + + // [2] Second call — invoke_agent: last message is small and kept intact + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'This is a small message that fits within the limit', + ); }, }) .start() @@ -840,110 +401,79 @@ describe('Vercel AI integration', () => { createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { test('creates embedding related spans with sendDefaultPii: false', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - // embed doEmbed span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }), - description: 'embeddings mock-model-id', - op: 'gen_ai.embeddings', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // embedMany doEmbed span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'embeddings mock-model-id', - op: 'gen_ai.embeddings', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; - await createRunner().expect({ transaction: expectedTransaction }).start().completed(); + // [0] embed doEmbed + expect(firstSpan!.name).toBe('embeddings mock-model-id'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + + // [1] embedMany doEmbed + expect(secondSpan!.name).toBe('embeddings mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(20); + }, + }) + .start() + .completed(); }); }); createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('creates embedding related spans with sendDefaultPii: true', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - // embed doEmbed span with input - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'Embedding test!', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 10, - }), - description: 'embeddings mock-model-id', - op: 'gen_ai.embeddings', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // embedMany doEmbed span with input - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: '["First input","Second input"]', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 20, - }), - description: 'embeddings mock-model-id', - op: 'gen_ai.embeddings', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; - await createRunner().expect({ transaction: expectedTransaction }).start().completed(); + // [0] embed doEmbed with input + expect(firstSpan!.name).toBe('embeddings mock-model-id'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('Embedding test!'); + + // [1] embedMany doEmbed with input + expect(secondSpan!.name).toBe('embeddings mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( + '["First input","Second input"]', + ); + }, + }) + .start() + .completed(); }); }); createEsmAndCjsTests(__dirname, 'scenario-conversation-id.mjs', 'instrument.mjs', (createRunner, test) => { test('does not overwrite conversation id set via Sentry.setConversationId with responseId from provider metadata', async () => { await createRunner() + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - op: 'gen_ai.invoke_agent', - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'conv-a', - }), - }), - expect.objectContaining({ - op: 'gen_ai.generate_content', - data: expect.objectContaining({ - 'gen_ai.conversation.id': 'conv-a', - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] invoke_agent with user-set conversation id + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['gen_ai.conversation.id'].value).toBe('conv-a'); + + // [1] generate_content also inherits the conversation id + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['gen_ai.conversation.id'].value).toBe('conv-a'); }, }) .start() @@ -960,22 +490,27 @@ describe('Vercel AI integration', () => { (createRunner, test) => { test('does not truncate input messages when enableTruncation is false', async () => { await createRunner() + .expect({ transaction: { transaction: 'main' } }) .expect({ - transaction: { - transaction: 'main', - spans: expect.arrayContaining([ - // Multiple messages should all be preserved (no popping to last message only) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: JSON.stringify([ - { role: 'user', content: longContent }, - { role: 'assistant', content: 'Some reply' }, - { role: 'user', content: 'Follow-up question' }, - ]), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, - }), - }), - ]), + span: container => { + expect(container.items).toHaveLength(2); + const [firstSpan, secondSpan] = container.items; + + // [0] invoke_agent — input messages preserved in full (no truncation) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]), + ); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + + // [1] generate_content + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs index b798e21228f5..ddc247cc2d41 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs index 5e898ee1949d..a76d206a0b61 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument.mjs @@ -7,4 +7,5 @@ Sentry.init({ tracesSampleRate: 1.0, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts index e59a5545d7cf..3fc6dd19f1ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts @@ -1,15 +1,10 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { Event } from '@sentry/node'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, - GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE, @@ -29,415 +24,75 @@ describe('Vercel AI integration (V5)', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - no telemetry config, should enable telemetry but not record inputs/outputs when sendDefaultPii: false - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Second span - explicitly enabled telemetry but recordInputs/recordOutputs not set, should not record when sendDefaultPii: false - expect.objectContaining({ - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.settings.maxRetries': 2, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.streaming': false, - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Third span - explicit telemetry enabled, should record inputs/outputs regardless of sendDefaultPii - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', - 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fourth span - doGenerate for explicit telemetry enabled call - expect.objectContaining({ - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.settings.maxRetries': 2, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.streaming': false, - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.id': expect.any(String), - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fifth span - tool call generateText span - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Sixth span - tool call doGenerate span - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Seventh span - tool call execution span - // Note: gen_ai.tool.description is NOT present when sendDefaultPii: false because ai.prompt.tools is not recorded - expect.objectContaining({ - data: { - 'vercel.ai.operationId': 'ai.toolCall', - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - - const EXPECTED_AVAILABLE_TOOLS_JSON = - '[{"type":"function","name":"getWeather","description":"Get the current weather for a location","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false}}]'; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - no telemetry config, should enable telemetry AND record inputs/outputs when sendDefaultPii: true - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the first span?"}]', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the first span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Second span - doGenerate for first call, should also include input/output fields when sendDefaultPii: true - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Third span - explicitly enabled telemetry, should record inputs/outputs regardless of sendDefaultPii - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fourth span - doGenerate for explicitly enabled telemetry call - expect.objectContaining({ - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.settings.maxRetries': 2, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.streaming': false, - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.id': expect.any(String), - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fifth span - tool call generateText span (should include prompts when sendDefaultPii: true) - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"What is the weather in San Francisco?"}]', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in San Francisco?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Sixth span - tool call doGenerate span (should include prompts when sendDefaultPii: true) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', - 'vercel.ai.prompt.toolChoice': expect.any(String), - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Seventh span - tool call execution span - expect.objectContaining({ - data: expect.objectContaining({ - 'vercel.ai.operationId': 'ai.toolCall', - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE]: 'Get the current weather for a location', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_INPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_TOOL_OUTPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates ai related spans with sendDefaultPii: false', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; + + // [0] First generateText — invoke_agent (no explicit telemetry, no PII) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + // [1] First generateText — generate_content (doGenerate, no PII) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + // [2] Second generateText — invoke_agent (explicit telemetry enabled) + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(thirdSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', + ); + + // [3] Second generateText — generate_content (doGenerate with PII) + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [4] Third generateText — invoke_agent (with tool call, no PII) + expect(fifthSpan!.name).toBe('invoke_agent'); + expect(fifthSpan!.status).toBe('ok'); + + // [5] Third generateText — generate_content (doGenerate) + expect(sixthSpan!.name).toBe('generate_content mock-model-id'); + expect(sixthSpan!.status).toBe('ok'); + + // [6] Tool execution + expect(seventhSpan!.name).toBe('execute_tool getWeather'); + expect(seventhSpan!.status).toBe('ok'); + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + }, + }) + .start() + .completed(); }); }, { @@ -453,7 +108,74 @@ describe('Vercel AI integration (V5)', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates ai related spans with sendDefaultPii: true', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; + + // [0] First generateText — invoke_agent (PII auto-enabled) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the first span?"}]', + ); + expect(firstSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', + ); + + // [1] First doGenerate with PII + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('First span here!'); + + // [2] Second generateText — invoke_agent (explicit telemetry) + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the second span?"}]', + ); + + // [3] Second doGenerate + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [4] Third generateText — invoke_agent (tool call prompt) + expect(fifthSpan!.name).toBe('invoke_agent'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"What is the weather in San Francisco?"}]', + ); + + // [5] Third doGenerate with available tools + expect(sixthSpan!.name).toBe('generate_content mock-model-id'); + expect(sixthSpan!.status).toBe('ok'); + expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeDefined(); + expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + // [6] Tool execution with PII + expect(seventhSpan!.name).toBe('execute_tool getWeather'); + expect(seventhSpan!.status).toBe('ok'); + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( + 'Get the current weather for a location', + ); + expect(seventhSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); + expect(seventhSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); + }, + }) + .start() + .completed(); }); }, { @@ -469,84 +191,6 @@ describe('Vercel AI integration (V5)', () => { 'instrument.mjs', (createRunner, test) => { test('captures error in tool', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.response.finishReason': 'tool-calls', - }, - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - }), - expect.objectContaining({ - data: { - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'vercel.ai.operationId': 'ai.toolCall', - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }, - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'internal_error', - }), - ]), - }; - - const expectedError = { - level: 'error', - tags: expect.objectContaining({ - 'vercel.ai.tool.name': 'getWeather', - 'vercel.ai.tool.callId': 'call-1', - }), - }; - let transactionEvent: Event | undefined; let errorEvent: Event | undefined; @@ -556,6 +200,27 @@ describe('Vercel AI integration (V5)', () => { transactionEvent = transaction; }, }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] invoke_agent + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + // [1] generate_content (doGenerate) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [2] execute_tool (errored) + expect(thirdSpan!.name).toBe('execute_tool getWeather'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + }, + }) .expect({ event: event => { errorEvent = event; @@ -565,10 +230,16 @@ describe('Vercel AI integration (V5)', () => { .completed(); expect(transactionEvent).toBeDefined(); - expect(transactionEvent).toMatchObject(expectedTransaction); + expect(transactionEvent!.transaction).toBe('main'); expect(errorEvent).toBeDefined(); - expect(errorEvent).toMatchObject(expectedError); + expect(errorEvent!.level).toBe('error'); + expect(errorEvent!.tags).toEqual( + expect.objectContaining({ + 'vercel.ai.tool.name': 'getWeather', + 'vercel.ai.tool.callId': 'call-1', + }), + ); // Trace id should be the same for the transaction and error event expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); @@ -587,7 +258,27 @@ describe('Vercel AI integration (V5)', () => { 'instrument.mjs', (createRunner, test) => { test('creates ai related spans with v5', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, , , fifthSpan, sixthSpan, seventhSpan] = container.items; + + // invoke_agent spans at [0], [2], [4] + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + // generate_content spans at [1], [3], [5] + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // execute_tool at [6] + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + }, + }) + .start() + .completed(); }); }, { diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument-with-pii.mjs index b798e21228f5..ddc247cc2d41 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument-with-pii.mjs @@ -8,4 +8,5 @@ Sentry.init({ sendDefaultPii: true, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument.mjs index 5e898ee1949d..a76d206a0b61 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/instrument.mjs @@ -7,4 +7,5 @@ Sentry.init({ tracesSampleRate: 1.0, transport: loggingTransport, integrations: [Sentry.vercelAIIntegration()], + streamGenAiSpans: true, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts index 1b030804f8d2..9c4e0ccd005e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts @@ -1,14 +1,11 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { Event } from '@sentry/node'; import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, - GEN_AI_OPERATION_NAME_ATTRIBUTE, GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, - GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE, @@ -28,419 +25,75 @@ describe('Vercel AI integration (V6)', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - no telemetry config, should enable telemetry but not record inputs/outputs when sendDefaultPii: false - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.request.headers.user-agent': expect.any(String), - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Second span - explicitly enabled telemetry but recordInputs/recordOutputs not set, should not record when sendDefaultPii: false - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.settings.maxRetries': 2, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.streaming': false, - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Third span - explicit telemetry enabled, should record inputs/outputs regardless of sendDefaultPii - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', - 'vercel.ai.request.headers.user-agent': expect.any(String), - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fourth span - doGenerate for explicit telemetry enabled call - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.settings.maxRetries': 2, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.streaming': false, - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fifth span - tool call generateText span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.request.headers.user-agent': expect.any(String), - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Sixth span - tool call doGenerate span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.request.headers.user-agent': expect.any(String), - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Seventh span - tool call execution span - // Note: gen_ai.tool.description is NOT present when sendDefaultPii: false because ai.prompt.tools is not recorded - expect.objectContaining({ - data: expect.objectContaining({ - 'vercel.ai.operationId': 'ai.toolCall', - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - - const EXPECTED_AVAILABLE_TOOLS_JSON = - '[{"type":"function","name":"getWeather","description":"Get the current weather for a location","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false}}]'; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - spans: expect.arrayContaining([ - // First span - no telemetry config, should enable telemetry AND record inputs/outputs when sendDefaultPii: true - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the first span?"}]', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the first span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Second span - doGenerate for first call, should also include input/output fields when sendDefaultPii: true - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Third span - explicitly enabled telemetry, should record inputs/outputs regardless of sendDefaultPii - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fourth span - doGenerate for explicitly enabled telemetry call - expect.objectContaining({ - data: expect.objectContaining({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.settings.maxRetries': 2, - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.streaming': false, - 'vercel.ai.response.finishReason': 'stop', - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Fifth span - tool call generateText span (should include prompts when sendDefaultPii: true) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.prompt': '[{"role":"user","content":"What is the weather in San Francisco?"}]', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in San Francisco?"}]', - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Sixth span - tool call doGenerate span (should include prompts when sendDefaultPii: true) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.request.headers.user-agent': expect.any(String), - [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), - [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: - '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', - 'vercel.ai.prompt.toolChoice': expect.any(String), - [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Seventh span - tool call execution span - expect.objectContaining({ - data: expect.objectContaining({ - 'vercel.ai.operationId': 'ai.toolCall', - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE]: 'Get the current weather for a location', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_INPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_TOOL_OUTPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - createEsmAndCjsTests( __dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates ai related spans with sendDefaultPii: false', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; + + // [0] First generateText — invoke_agent (no explicit telemetry, no PII) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + // [1] First generateText — generate_content (doGenerate, no PII) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + // [2] Second generateText — invoke_agent (explicit telemetry enabled) + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(thirdSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', + ); + + // [3] Second generateText — generate_content (doGenerate with PII) + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [4] Third generateText — invoke_agent (with tool call, no PII) + expect(fifthSpan!.name).toBe('invoke_agent'); + expect(fifthSpan!.status).toBe('ok'); + + // [5] Third generateText — generate_content (doGenerate) + expect(sixthSpan!.name).toBe('generate_content mock-model-id'); + expect(sixthSpan!.status).toBe('ok'); + + // [6] Tool execution + expect(seventhSpan!.name).toBe('execute_tool getWeather'); + expect(seventhSpan!.status).toBe('ok'); + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + }, + }) + .start() + .completed(); }); }, { @@ -456,7 +109,71 @@ describe('Vercel AI integration (V6)', () => { 'instrument-with-pii.mjs', (createRunner, test) => { test('creates ai related spans with sendDefaultPii: true', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; + + // [0] First generateText — invoke_agent (PII auto-enabled) + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"Where is the first span?"}]', + ); + expect(firstSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', + ); + + // [1] First doGenerate with PII + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('First span here!'); + + // [2] Second generateText — invoke_agent (explicit telemetry) + expect(thirdSpan!.name).toBe('invoke_agent'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + // [3] Second doGenerate + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [4] Third generateText — invoke_agent (tool call prompt) + expect(fifthSpan!.name).toBe('invoke_agent'); + expect(fifthSpan!.status).toBe('ok'); + expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + '[{"role":"user","content":"What is the weather in San Francisco?"}]', + ); + + // [5] Third doGenerate with available tools + expect(sixthSpan!.name).toBe('generate_content mock-model-id'); + expect(sixthSpan!.status).toBe('ok'); + expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(sixthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeDefined(); + expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + // [6] Tool execution with PII + expect(seventhSpan!.name).toBe('execute_tool getWeather'); + expect(seventhSpan!.status).toBe('ok'); + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(seventhSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( + 'Get the current weather for a location', + ); + expect(seventhSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); + expect(seventhSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); + }, + }) + .start() + .completed(); }); }, { @@ -472,86 +189,6 @@ describe('Vercel AI integration (V6)', () => { 'instrument.mjs', (createRunner, test) => { test('captures error in tool', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText', - 'vercel.ai.pipeline.name': 'generateText', - 'vercel.ai.request.headers.user-agent': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - 'vercel.ai.response.finishReason': 'tool-calls', - }), - description: 'invoke_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - }), - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - 'vercel.ai.model.provider': 'mock-provider', - 'vercel.ai.operationId': 'ai.generateText.doGenerate', - 'vercel.ai.pipeline.name': 'generateText.doGenerate', - 'vercel.ai.request.headers.user-agent': expect.any(String), - 'vercel.ai.response.finishReason': 'tool-calls', - 'vercel.ai.response.id': expect.any(String), - 'vercel.ai.response.model': 'mock-model-id', - 'vercel.ai.response.timestamp': expect.any(String), - 'vercel.ai.settings.maxRetries': 2, - 'vercel.ai.streaming': false, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), - [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'vercel.ai.operationId': 'ai.toolCall', - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'internal_error', - }), - ]), - }; - - const expectedError = { - level: 'error', - tags: expect.objectContaining({ - 'vercel.ai.tool.name': 'getWeather', - 'vercel.ai.tool.callId': 'call-1', - }), - }; - let transactionEvent: Event | undefined; let errorEvent: Event | undefined; @@ -561,6 +198,27 @@ describe('Vercel AI integration (V6)', () => { transactionEvent = transaction; }, }) + .expect({ + span: container => { + expect(container.items).toHaveLength(3); + const [firstSpan, secondSpan, thirdSpan] = container.items; + + // [0] invoke_agent + expect(firstSpan!.name).toBe('invoke_agent'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + // [1] generate_content (doGenerate) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // [2] execute_tool (errored) + expect(thirdSpan!.name).toBe('execute_tool getWeather'); + expect(thirdSpan!.status).toBe('error'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + }, + }) .expect({ event: event => { errorEvent = event; @@ -570,10 +228,16 @@ describe('Vercel AI integration (V6)', () => { .completed(); expect(transactionEvent).toBeDefined(); - expect(transactionEvent).toMatchObject(expectedTransaction); + expect(transactionEvent!.transaction).toBe('main'); expect(errorEvent).toBeDefined(); - expect(errorEvent).toMatchObject(expectedError); + expect(errorEvent!.level).toBe('error'); + expect(errorEvent!.tags).toEqual( + expect.objectContaining({ + 'vercel.ai.tool.name': 'getWeather', + 'vercel.ai.tool.callId': 'call-1', + }), + ); // Trace id should be the same for the transaction and error event expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); @@ -592,7 +256,27 @@ describe('Vercel AI integration (V6)', () => { 'instrument.mjs', (createRunner, test) => { test('creates ai related spans with v6', async () => { - await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(7); + const [firstSpan, secondSpan, , , fifthSpan, sixthSpan, seventhSpan] = container.items; + + // invoke_agent spans at [0], [2], [4] + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + // generate_content spans at [1], [3], [5] + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + // execute_tool at [6] + expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + }, + }) + .start() + .completed(); }); }, { @@ -608,71 +292,46 @@ describe('Vercel AI integration (V6)', () => { 'instrument.mjs', (createRunner, test) => { test('creates spans for ToolLoopAgent with tool calls', async () => { - const expectedTransaction = { - transaction: 'main', - spans: expect.arrayContaining([ - // ToolLoopAgent outer span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', - }), - description: 'invoke_agent weather_agent', - op: 'gen_ai.invoke_agent', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // First doGenerate span (returns tool-calls) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Tool execution span - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', - [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', - [GEN_AI_TOOL_TYPE_ATTRIBUTE]: 'function', - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - }), - description: 'execute_tool getWeather', - op: 'gen_ai.execute_tool', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - // Second doGenerate span (returns final text) - expect.objectContaining({ - data: expect.objectContaining({ - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.generate_content', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.vercelai.otel', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, - [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, - [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], - }), - description: 'generate_content mock-model-id', - op: 'gen_ai.generate_content', - origin: 'auto.vercelai.otel', - status: 'ok', - }), - ]), - }; - - await createRunner().expect({ transaction: expectedTransaction }).start().completed(); + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + expect(container.items).toHaveLength(4); + const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; + + // [0] invoke_agent (ToolLoopAgent outer span) + expect(firstSpan!.name).toBe('invoke_agent weather_agent'); + expect(firstSpan!.status).toBe('ok'); + expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + + // [1] First doGenerate (returns tool-calls) + expect(secondSpan!.name).toBe('generate_content mock-model-id'); + expect(secondSpan!.status).toBe('ok'); + expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["tool-calls"]'); + expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + + // [2] Tool execution + expect(thirdSpan!.name).toBe('execute_tool getWeather'); + expect(thirdSpan!.status).toBe('ok'); + expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(thirdSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + + // [3] Second doGenerate (returns final text) + expect(fourthSpan!.name).toBe('generate_content mock-model-id'); + expect(fourthSpan!.status).toBe('ok'); + expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["stop"]'); + expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(25); + }, + }) + .start() + .completed(); }); }, { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 766a0a4ecfdc..94cc00e19f88 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -12,6 +12,7 @@ import type { Scope } from './scope'; import { updateSession } from './session'; import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; import { isStreamedBeforeSendSpanCallback } from './tracing/spans/beforeSendSpan'; +import { extractGenAiSpansFromEvent } from './tracing/spans/extractGenAiSpans'; import { DEFAULT_TRANSPORT_BUFFER_SIZE } from './transports/base'; import type { Breadcrumb, BreadcrumbHint, FetchBreadcrumbHint, XhrBreadcrumbHint } from './types-hoist/breadcrumb'; import type { CheckIn, MonitorConfig } from './types-hoist/checkin'; @@ -527,12 +528,20 @@ export abstract class Client { public sendEvent(event: Event, hint: EventHint = {}): void { this.emit('beforeSendEvent', event, hint); + // Extract gen_ai spans from transaction and convert to span v2 format. + // This mutates event.spans to remove the extracted spans. + const genAiSpanItem = extractGenAiSpansFromEvent(event, this); + let env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel); for (const attachment of hint.attachments || []) { env = addItemToEnvelope(env, createAttachmentEnvelopeItem(attachment)); } + if (genAiSpanItem) { + env = addItemToEnvelope(env, genAiSpanItem); + } + // sendEnvelope should not throw // eslint-disable-next-line @typescript-eslint/no-floating-promises this.sendEnvelope(env).then(sendResponse => this.emit('afterSendEvent', event, sendResponse)); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index d9ab115b94cd..92c4617f29c2 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -392,8 +392,12 @@ export class SentrySpan implements Span { // remove internal root span attributes we don't need to send. /* eslint-disable @typescript-eslint/no-dynamic-delete */ delete this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + let hasGenAiSpans = false; spans.forEach(span => { delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + if (span.op?.startsWith('gen_ai.')) { + hasGenAiSpans = true; + } }); // eslint-enabled-next-line @typescript-eslint/no-dynamic-delete @@ -415,6 +419,7 @@ export class SentrySpan implements Span { capturedSpanScope, capturedSpanIsolationScope, dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), + hasGenAiSpans, }, request: normalizedRequest, ...(source && { diff --git a/packages/core/src/tracing/spans/extractGenAiSpans.ts b/packages/core/src/tracing/spans/extractGenAiSpans.ts new file mode 100644 index 000000000000..42c47804728a --- /dev/null +++ b/packages/core/src/tracing/spans/extractGenAiSpans.ts @@ -0,0 +1,58 @@ +import type { Client } from '../../client'; +import type { SpanContainerItem } from '../../types-hoist/envelope'; +import type { Event } from '../../types-hoist/event'; +import { isBrowser } from '../../utils/isBrowser'; +import { hasSpanStreamingEnabled } from './hasSpanStreamingEnabled'; +import { spanJsonToSerializedStreamedSpan } from './spanJsonToStreamedSpan'; + +/** + * Extracts gen_ai spans from a transaction event, converts them to span v2 format, + * and returns them as a SpanContainerItem. + * + * Only applies to static mode (non-streaming) transactions. + * + * WARNING: This function mutates `event.spans` by removing the extracted gen_ai spans + * from the array. Call this before creating the event envelope so the transaction + * item does not include the extracted spans. + */ +export function extractGenAiSpansFromEvent(event: Event, client: Client): SpanContainerItem | undefined { + if ( + event.type !== 'transaction' || + !event.spans?.length || + !event.sdkProcessingMetadata?.hasGenAiSpans || + !client.getOptions().streamGenAiSpans || + hasSpanStreamingEnabled(client) + ) { + return undefined; + } + + const genAiSpans = []; + const remainingSpans = []; + + for (const span of event.spans) { + if (span.op?.startsWith('gen_ai.')) { + genAiSpans.push(spanJsonToSerializedStreamedSpan(span)); + } else { + remainingSpans.push(span); + } + } + + if (genAiSpans.length === 0) { + return undefined; + } + + event.spans = remainingSpans; + + const inferSetting = client.getOptions().sendDefaultPii ? 'auto' : 'never'; + + return [ + { type: 'span', item_count: genAiSpans.length, content_type: 'application/vnd.sentry.items.span.v2+json' }, + { + version: 2, + ...(isBrowser() && { + ingest_settings: { infer_ip: inferSetting, infer_user_agent: inferSetting }, + }), + items: genAiSpans, + }, + ]; +} diff --git a/packages/core/src/tracing/spans/spanJsonToStreamedSpan.ts b/packages/core/src/tracing/spans/spanJsonToStreamedSpan.ts new file mode 100644 index 000000000000..4dfd6c5202b9 --- /dev/null +++ b/packages/core/src/tracing/spans/spanJsonToStreamedSpan.ts @@ -0,0 +1,23 @@ +import type { RawAttributes } from '../../attributes'; +import type { SerializedStreamedSpan, SpanJSON, StreamedSpanJSON } from '../../types-hoist/span'; +import { streamedSpanJsonToSerializedSpan } from '../../utils/spanUtils'; + +/** + * Converts a v1 SpanJSON (from a legacy transaction) to a serialized v2 StreamedSpan. + */ +export function spanJsonToSerializedStreamedSpan(span: SpanJSON): SerializedStreamedSpan { + const streamedSpan: StreamedSpanJSON = { + trace_id: span.trace_id, + span_id: span.span_id, + parent_span_id: span.parent_span_id, + name: span.description || '', + start_timestamp: span.start_timestamp, + end_timestamp: span.timestamp || span.start_timestamp, + status: !span.status || span.status === 'ok' || span.status === 'cancelled' ? 'ok' : 'error', + is_segment: false, + attributes: { ...(span.data as RawAttributes>) }, + links: span.links, + }; + + return streamedSpanJsonToSerializedSpan(streamedSpan); +} diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index c6ff4c784dde..4abdb0af5eae 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -325,8 +325,13 @@ export function processVercelAiSpanAttributes(attributes: Record; export type SessionEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index a1fc1e074a75..db65585d9e43 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -553,6 +553,15 @@ export interface ClientOptions { trace_id: expect.stringMatching(/^[a-f0-9]{32}$/), transaction: 'test', }, + hasGenAiSpans: false, }, spans: [], start_timestamp: 1, diff --git a/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts b/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts new file mode 100644 index 000000000000..e3286fb76a97 --- /dev/null +++ b/packages/core/test/lib/tracing/spans/extractGenAiSpans.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, it } from 'vitest'; +import type { Event } from '../../../../src/types-hoist/event'; +import type { SpanJSON } from '../../../../src/types-hoist/span'; +import { extractGenAiSpansFromEvent } from '../../../../src/tracing/spans/extractGenAiSpans'; +import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; + +function makeSpanJSON(overrides: Partial = {}): SpanJSON { + return { + span_id: 'abc123def456789a', + trace_id: '00112233445566778899aabbccddeeff', + start_timestamp: 1000, + data: {}, + ...overrides, + }; +} + +function makeTransactionEvent(spans: SpanJSON[]): Event { + return { + type: 'transaction', + transaction: 'GET /api/chat', + release: '1.0.0', + environment: 'production', + contexts: { + trace: { + span_id: 'root0000deadbeef', + trace_id: '00112233445566778899aabbccddeeff', + }, + }, + sdkProcessingMetadata: { + hasGenAiSpans: true, + }, + spans, + }; +} + +function makeClient(options: Partial[0]> = {}): TestClient { + return new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://dsn@ingest.f00.f00/1', + streamGenAiSpans: true, + ...options, + }), + ); +} + +describe('extractGenAiSpansFromEvent', () => { + it('extracts gen_ai spans and removes them from the event', () => { + const genAiSpan = makeSpanJSON({ + span_id: 'genai001', + op: 'gen_ai.chat', + description: 'chat gpt-4', + timestamp: 1005, + }); + const httpSpan = makeSpanJSON({ + span_id: 'http001', + op: 'http.client', + description: 'GET /api', + timestamp: 1002, + }); + + const event = makeTransactionEvent([genAiSpan, httpSpan], true); + const result = extractGenAiSpansFromEvent(event, makeClient()); + + expect(result).toBeDefined(); + const [headers, payload] = result!; + expect(headers.type).toBe('span'); + expect(headers.item_count).toBe(1); + expect(headers.content_type).toBe('application/vnd.sentry.items.span.v2+json'); + expect(payload.version).toBe(2); + expect(payload.items).toHaveLength(1); + expect(payload.items[0]!.span_id).toBe('genai001'); + expect(payload.items[0]!.name).toBe('chat gpt-4'); + + expect(event.spans).toHaveLength(1); + expect(event.spans![0]!.span_id).toBe('http001'); + }); + + it('extracts multiple gen_ai spans', () => { + const chatSpan = makeSpanJSON({ span_id: 'chat001', op: 'gen_ai.chat', description: 'chat' }); + const embeddingsSpan = makeSpanJSON({ span_id: 'embed001', op: 'gen_ai.embeddings', description: 'embed' }); + const agentSpan = makeSpanJSON({ span_id: 'agent001', op: 'gen_ai.invoke_agent', description: 'agent' }); + const dbSpan = makeSpanJSON({ span_id: 'db001', op: 'db.query', description: 'SELECT *' }); + + const event = makeTransactionEvent([chatSpan, embeddingsSpan, dbSpan, agentSpan], true); + const result = extractGenAiSpansFromEvent(event, makeClient()); + + expect(result).toBeDefined(); + expect(result![0].item_count).toBe(3); + expect(result![1].items).toHaveLength(3); + expect(result![1].items.map(s => s.span_id)).toEqual(['chat001', 'embed001', 'agent001']); + + expect(event.spans).toHaveLength(1); + expect(event.spans![0]!.span_id).toBe('db001'); + }); + + it('returns undefined when hasGenAiSpans flag is not set', () => { + const event: Event = { + type: 'transaction', + spans: [makeSpanJSON({ op: 'gen_ai.chat' })], + sdkProcessingMetadata: {}, + }; + + expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined(); + expect(event.spans).toHaveLength(1); + }); + + it('returns undefined when there are no gen_ai spans', () => { + const event = makeTransactionEvent([makeSpanJSON({ op: 'http.client' }), makeSpanJSON({ op: 'db.query' })]); + + expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined(); + expect(event.spans).toHaveLength(2); + }); + + it('returns undefined when event has no spans', () => { + const event = makeTransactionEvent([]); + expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined(); + }); + + it('returns undefined when event is not a transaction', () => { + const event: Event = { type: undefined, spans: [makeSpanJSON({ op: 'gen_ai.chat' })] }; + expect(extractGenAiSpansFromEvent(event, makeClient())).toBeUndefined(); + }); + + it('returns undefined when streamGenAiSpans is not enabled', () => { + const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })]); + const client = makeClient({ streamGenAiSpans: false }); + + expect(extractGenAiSpansFromEvent(event, client)).toBeUndefined(); + expect(event.spans).toHaveLength(1); + }); + + it('returns undefined when span streaming is enabled', () => { + const event = makeTransactionEvent([makeSpanJSON({ op: 'gen_ai.chat' })]); + const client = makeClient({ traceLifecycle: 'stream' }); + + expect(extractGenAiSpansFromEvent(event, client)).toBeUndefined(); + expect(event.spans).toHaveLength(1); + }); + + it('preserves parent_span_id pointing to v1 spans', () => { + const genAiSpan = makeSpanJSON({ + span_id: 'genai001', + parent_span_id: 'http001', + op: 'gen_ai.chat', + }); + const httpSpan = makeSpanJSON({ + span_id: 'http001', + op: 'http.client', + }); + + const event = makeTransactionEvent([httpSpan, genAiSpan]); + const result = extractGenAiSpansFromEvent(event, makeClient()); + + expect(result![1].items[0]!.parent_span_id).toBe('http001'); + expect(event.spans).toHaveLength(1); + expect(event.spans![0]!.span_id).toBe('http001'); + }); +}); diff --git a/packages/core/test/lib/tracing/spans/spanJsonToStreamedSpan.test.ts b/packages/core/test/lib/tracing/spans/spanJsonToStreamedSpan.test.ts new file mode 100644 index 000000000000..a42ce3468e65 --- /dev/null +++ b/packages/core/test/lib/tracing/spans/spanJsonToStreamedSpan.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; +import type { SpanJSON } from '../../../../src/types-hoist/span'; +import { spanJsonToSerializedStreamedSpan } from '../../../../src/tracing/spans/spanJsonToStreamedSpan'; + +function makeSpanJSON(overrides: Partial = {}): SpanJSON { + return { + span_id: 'abc123def456789a', + trace_id: '00112233445566778899aabbccddeeff', + start_timestamp: 1000, + data: {}, + ...overrides, + }; +} + +describe('spanJsonToSerializedStreamedSpan', () => { + it('maps basic SpanJSON fields to StreamedSpan fields', () => { + const span = makeSpanJSON({ + description: 'chat gpt-4', + timestamp: 1005, + status: 'ok', + op: 'gen_ai.chat', + origin: 'auto.ai.openai', + parent_span_id: 'parent00deadbeef', + }); + + const result = spanJsonToSerializedStreamedSpan(span); + + expect(result.name).toBe('chat gpt-4'); + expect(result.start_timestamp).toBe(1000); + expect(result.end_timestamp).toBe(1005); + expect(result.status).toBe('ok'); + expect(result.is_segment).toBe(false); + expect(result.span_id).toBe('abc123def456789a'); + expect(result.trace_id).toBe('00112233445566778899aabbccddeeff'); + expect(result.parent_span_id).toBe('parent00deadbeef'); + }); + + it('uses empty string for name when description is undefined', () => { + const result = spanJsonToSerializedStreamedSpan(makeSpanJSON({ description: undefined })); + expect(result.name).toBe(''); + }); + + it('uses start_timestamp as end_timestamp when timestamp is undefined', () => { + const result = spanJsonToSerializedStreamedSpan(makeSpanJSON({ timestamp: undefined })); + expect(result.end_timestamp).toBe(1000); + }); + + it('maps v1 status strings to v2 ok/error', () => { + const cases: Array<[string | undefined, 'ok' | 'error']> = [ + [undefined, 'ok'], + ['ok', 'ok'], + ['cancelled', 'ok'], + ['internal_error', 'error'], + ['not_found', 'error'], + ['unknown_error', 'error'], + ]; + + for (const [v1Status, expected] of cases) { + const result = spanJsonToSerializedStreamedSpan(makeSpanJSON({ status: v1Status })); + expect(result.status).toBe(expected); + } + }); + + it('preserves existing span data attributes', () => { + const span = makeSpanJSON({ + data: { + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-4', + 'gen_ai.usage.input_tokens': 100, + 'gen_ai.usage.output_tokens': 50, + }, + }); + + const result = spanJsonToSerializedStreamedSpan(span); + + expect(result.attributes?.['gen_ai.system']).toEqual({ type: 'string', value: 'openai' }); + expect(result.attributes?.['gen_ai.request.model']).toEqual({ type: 'string', value: 'gpt-4' }); + expect(result.attributes?.['gen_ai.usage.input_tokens']).toEqual({ type: 'integer', value: 100 }); + expect(result.attributes?.['gen_ai.usage.output_tokens']).toEqual({ type: 'integer', value: 50 }); + }); + + it('carries over links', () => { + const span = makeSpanJSON({ + links: [{ trace_id: 'aabb', span_id: 'ccdd', sampled: true, attributes: { foo: 'bar' } }], + }); + + const result = spanJsonToSerializedStreamedSpan(span); + + expect(result.links).toEqual([ + { trace_id: 'aabb', span_id: 'ccdd', sampled: true, attributes: { foo: { type: 'string', value: 'bar' } } }, + ]); + }); +}); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index f02df1d9d56c..aed57b52e58e 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -203,8 +203,11 @@ export class SentrySpanExporter { // We'll recursively add all the child spans to this array const spans = transactionEvent.spans || []; + let hasGenAiSpans = false; for (const child of root.children) { - createAndFinishSpanForOtelSpan(child, spans, sentSpans); + if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) { + hasGenAiSpans = true; + } } // spans.sort() mutates the array, but we do not use this anymore after this point @@ -214,6 +217,13 @@ export class SentrySpanExporter { ? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT) : spans; + if (hasGenAiSpans) { + transactionEvent.sdkProcessingMetadata = { + ...transactionEvent.sdkProcessingMetadata, + hasGenAiSpans: true, + }; + } + const measurements = timedEventsToMeasurements(span.events); if (measurements) { transactionEvent.measurements = measurements; @@ -330,7 +340,10 @@ export function createTransactionForOtelSpan(span: ReadableSpan): TransactionEve return transactionEvent; } -function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set): void { +/** + * Returns `true` if this span or any descendant is a gen_ai span. + */ +function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set): boolean { const span = node.span; if (span) { @@ -341,10 +354,13 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentS // If this span should be dropped, we still want to create spans for the children of this if (shouldDrop) { + let hasGenAiSpans = false; node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, sentSpans); + if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) { + hasGenAiSpans = true; + } }); - return; + return hasGenAiSpans; } const span_id = span.spanContext().spanId; @@ -381,9 +397,13 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentS spans.push(spanJSON); + let hasGenAiSpans = !!op?.startsWith('gen_ai.'); node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, sentSpans); + if (createAndFinishSpanForOtelSpan(child, spans, sentSpans)) { + hasGenAiSpans = true; + } }); + return hasGenAiSpans; } function getSpanData(span: ReadableSpan): { From e30c6f7c6e622cbab1d6794d5b054d7385e01815 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 12 May 2026 16:20:16 +0900 Subject: [PATCH 22/32] chore(ci): Unflake gen_ai tests (#20813) Instead of asserting gen_ai spans order, we follow the common streaming approach of finding the spans by name and asserting existence. Closes: #20812 --- .../suites/tracing/google-genai/test.ts | 99 +- .../suites/tracing/langchain/test.ts | 56 +- .../suites/tracing/langgraph/test.ts | 57 +- .../suites/tracing/anthropic/test.ts | 690 +++++------ .../suites/tracing/google-genai/test.ts | 555 ++++----- .../suites/tracing/langchain/test.ts | 393 ++++--- .../suites/tracing/langchain/v1/test.ts | 294 ++--- .../suites/tracing/langgraph/test.ts | 275 ++--- .../tracing/openai/openai-tool-calls/test.ts | 479 +++++--- .../suites/tracing/openai/test.ts | 1019 +++++++++++------ .../suites/tracing/openai/v6/test.ts | 693 ++++++----- .../suites/tracing/vercelai/test.ts | 563 +++++---- .../suites/tracing/vercelai/v5/test.ts | 295 +++-- .../suites/tracing/vercelai/v6/test.ts | 379 +++--- 14 files changed, 3481 insertions(+), 2366 deletions(-) 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 98f730af9716..2724c8017c7c 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 @@ -29,52 +29,83 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) = const container = envelope[1]?.[1]?.[1] as any; expect(container).toBeDefined(); expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gemini-1.5-pro', + 'embeddings text-embedding-004', + 'generate_content gemini-1.5-flash', + ]); - // [0] chat gemini-1.5-pro - expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); - expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.status).toBe('ok'); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(chatSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + expect(chatSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); + expect(chatSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); + expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gemini-1.5-pro', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(chatSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); + expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); + expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); - // [1] generate_content gemini-1.5-flash - expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'generate_content', }); - expect(secondSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.generate_content' }); - expect(secondSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(generateContentSpan!.attributes['sentry.op']).toEqual({ + type: 'string', + value: 'gen_ai.generate_content', + }); + expect(generateContentSpan!.attributes['sentry.origin']).toEqual({ + type: 'string', + value: 'auto.ai.google_genai', + }); + expect(generateContentSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'google_genai', + }); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gemini-1.5-flash', }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]).toEqual({ type: 'double', value: 0.9 }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 100 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.7, + }); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]).toEqual({ type: 'double', value: 0.9 }); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 100, + }); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 20, + }); - // [2] embeddings text-embedding-004 - expect(thirdSpan!.name).toBe('embeddings text-embedding-004'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings' }); - expect(thirdSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.embeddings' }); - expect(thirdSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const embeddingsSpan = container.items.find(span => span.name === 'embeddings text-embedding-004'); + expect(embeddingsSpan).toBeDefined(); + expect(embeddingsSpan!.status).toBe('ok'); + expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'embeddings', + }); + expect(embeddingsSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.embeddings' }); + expect(embeddingsSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.google_genai' }); + expect(embeddingsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'google_genai' }); + expect(embeddingsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-004', }); 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 d05255f2c437..6968d57bfef9 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langchain/test.ts @@ -29,38 +29,42 @@ it('traces langchain chat model, chain, and tool invocations', async ({ signal } const container = envelope[1]?.[1]?.[1] as any; expect(container).toBeDefined(); expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chain my_test_chain', + 'chat claude-3-5-sonnet-20241022', + 'execute_tool search_tool', + ]); - // [0] chat claude-3-5-sonnet-20241022 - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); - expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'anthropic' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.status).toBe('ok'); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); + expect(chatSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.chat' }); + expect(chatSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); + expect(chatSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'anthropic' }); + expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'claude-3-5-sonnet-20241022', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 100 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); + expect(chatSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 100 }); + expect(chatSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); + expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); - // [1] chain my_test_chain - expect(secondSpan!.name).toBe('chain my_test_chain'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); - expect(secondSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' }); - expect(secondSpan!.attributes['langchain.chain.name']).toEqual({ type: 'string', value: 'my_test_chain' }); + const chainSpan = container.items.find(span => span.name === 'chain my_test_chain'); + expect(chainSpan).toBeDefined(); + expect(chainSpan!.status).toBe('ok'); + expect(chainSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); + expect(chainSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' }); + expect(chainSpan!.attributes['langchain.chain.name']).toEqual({ type: 'string', value: 'my_test_chain' }); - // [2] execute_tool search_tool - expect(thirdSpan!.name).toBe('execute_tool search_tool'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); - expect(thirdSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.execute_tool' }); - expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'search_tool' }); + const toolSpan = container.items.find(span => span.name === 'execute_tool search_tool'); + expect(toolSpan).toBeDefined(); + expect(toolSpan!.status).toBe('ok'); + expect(toolSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' }); + expect(toolSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.execute_tool' }); + expect(toolSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'search_tool' }); }) .start(signal); await runner.makeRequest('get', '/'); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts index 4c8623654273..542f29ad7206 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/langgraph/test.ts @@ -29,47 +29,62 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => { expect(container).toBeDefined(); expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'create_agent weather_assistant', + 'invoke_agent weather_assistant', + ]); - // [0] create_agent weather_assistant - expect(firstSpan!.name).toBe('create_agent weather_assistant'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant'); + expect(createAgentSpan).toBeDefined(); + expect(createAgentSpan!.status).toBe('ok'); + expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'create_agent', }); - expect(firstSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.create_agent' }); - expect(firstSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); - expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]).toEqual({ + expect(createAgentSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.create_agent' }); + expect(createAgentSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); + expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'weather_assistant', }); - // [1] invoke_agent weather_assistant - expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent weather_assistant'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.status).toBe('ok'); + expect(invokeAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'invoke_agent', }); - expect(secondSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' }); - expect(secondSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); - expect(secondSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]).toEqual({ + expect(invokeAgentSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' }); + expect(invokeAgentSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langgraph' }); + expect(invokeAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'weather_assistant', }); - expect(secondSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE]).toEqual({ + expect(invokeAgentSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'weather_assistant', }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather in SF?"}]', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'mock-model' }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30 }); + expect(invokeAgentSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'mock-model', + }); + expect(invokeAgentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 20, + }); + expect(invokeAgentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(invokeAgentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 30, + }); }) .start(signal); await runner.makeRequest('get', '/'); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 63a965345b25..5071e62af9d4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -62,49 +62,53 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] messages.create — basic message completion without PII - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - - // [1] messages.create with error-model — error handling - expect(secondSpan!.name).toBe('chat error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); - - // [2] messages.countTokens — token counting - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - - // [3] models.retrieve - expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('models'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - expect(fourthSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + const completionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_mock123', + ); + expect(completionSpan).toBeDefined(); + expect(completionSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(completionSpan!.status).toBe('ok'); + expect(completionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(completionSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(completionSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(completionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(completionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(completionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(completionSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(completionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(completionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(completionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(completionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + + const tokenCountingSpan = container.items.find( + span => + span.name === 'chat claude-3-haiku-20240307' && + span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE] === undefined, + ); + expect(tokenCountingSpan).toBeDefined(); + expect(tokenCountingSpan!.status).toBe('ok'); + expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(tokenCountingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + + const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); + expect(modelsSpan).toBeDefined(); + expect(modelsSpan!.status).toBe('ok'); + expect(modelsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('models'); + expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + expect(modelsSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(modelsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(modelsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(modelsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(modelsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); }, }) .start() @@ -124,27 +128,24 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] .withResponse() — non-streaming - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_withresponse'); - - // [1] .asResponse() — non-streaming - expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_withresponse'); - - // [2] streaming .withResponse() - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_withresponse'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + const nonStreamingSpans = container.items.filter( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_withresponse', + ); + expect(nonStreamingSpans).toHaveLength(2); + for (const span of nonStreamingSpans) { + expect(span.name).toBe('chat claude-3-haiku-20240307'); + expect(span.status).toBe('ok'); + expect(span.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + } + + const streamingSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_stream_withresponse', + ); + expect(streamingSpan).toBeDefined(); + expect(streamingSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(streamingSpan!.status).toBe('ok'); + expect(streamingSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); }, }) .start() @@ -160,32 +161,38 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(5); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan] = container.items; + const completionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_mock123', + ); + expect(completionSpan).toBeDefined(); + expect(completionSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(completionSpan!.status).toBe('ok'); + + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + + const tokenCountingSpan = container.items.find( + span => + span.name === 'chat claude-3-haiku-20240307' && + span.status === 'ok' && + span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE] === undefined, + ); + expect(tokenCountingSpan).toBeDefined(); + expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - // [0] messages.create — basic message completion - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); - - // [1] messages.create with error-model — error handling - expect(secondSpan!.name).toBe('chat error-model'); - expect(secondSpan!.status).toBe('error'); - - // [2] messages.countTokens — token counting - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - - // [3] models.retrieve - expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - - // [4] messages.create stream: true + messages.stream (both share this span due to pre-existing bug) - expect(fifthSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream123'); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); + expect(modelsSpan).toBeDefined(); + expect(modelsSpan!.status).toBe('ok'); + expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + + const streamingSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_stream123', + ); + expect(streamingSpan).toBeDefined(); + expect(streamingSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(streamingSpan!.status).toBe('ok'); + expect(streamingSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); }, }) .expect({ event: EXPECTED_STREAM_EVENT_HANDLER_MESSAGE }) @@ -202,61 +209,67 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(5); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan] = container.items; - - // [0] messages.create — basic message completion with PII - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - '[{"role":"user","content":"What is the capital of France?"}]', + const completionSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"What is the capital of France?"}]', ); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from Anthropic mock!'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - - // [1] messages.create with error-model — error handling with PII - expect(secondSpan!.name).toBe('chat error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - '[{"role":"user","content":"This will fail"}]', + expect(completionSpan).toBeDefined(); + expect(completionSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(completionSpan!.status).toBe('ok'); + expect(completionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(completionSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(completionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(completionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(completionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(completionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); + expect(completionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from Anthropic mock!'); + expect(completionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(completionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(completionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(completionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(completionSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(completionSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + + const errorSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"This will fail"}]', ); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.name).toBe('chat error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); - // [2] messages.countTokens — token counting with PII (response text records input_tokens as "15") - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('15'); + const tokenCountingSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]?.value === '15', + ); + expect(tokenCountingSpan).toBeDefined(); + expect(tokenCountingSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(tokenCountingSpan!.status).toBe('ok'); + expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - // [3] models.retrieve with PII - expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); + expect(modelsSpan).toBeDefined(); + expect(modelsSpan!.status).toBe('ok'); + expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - // [4] messages.create stream: true + messages.stream (both share this span due to pre-existing bug) // TODO: messages.stream() should produce its own distinct gen_ai span, but it // currently does not (pre-existing bug). Once fixed, add an additional indexed span assertion. - expect(fifthSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream123'); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); - expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + const streamingSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_stream123', + ); + expect(streamingSpan).toBeDefined(); + expect(streamingSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(streamingSpan!.status).toBe('ok'); + expect(streamingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(streamingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(streamingSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(streamingSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(streamingSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + expect(streamingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(streamingSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(streamingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); }, }) .expect({ event: EXPECTED_STREAM_EVENT_HANDLER_MESSAGE }) @@ -273,43 +286,47 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(5); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan] = container.items; - - // [0] messages.create — chat span with custom PII options (input messages + response text recorded) - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_mock123'); - - // [1] messages.create with error-model — error handling - expect(secondSpan!.name).toBe('chat error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - - // [2] messages.countTokens — token counting with options (response text = "15") - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('15'); - - // [3] models.retrieve with options - expect(fourthSpan!.name).toBe('models claude-3-haiku-20240307'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('models'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - - // [4] messages.create stream: true + messages.stream (share this span due to pre-existing bug) - expect(fifthSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream123'); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + const completionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_mock123', + ); + expect(completionSpan).toBeDefined(); + expect(completionSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(completionSpan!.status).toBe('ok'); + expect(completionSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(completionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(completionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + const tokenCountingSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]?.value === '15', + ); + expect(tokenCountingSpan).toBeDefined(); + expect(tokenCountingSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(tokenCountingSpan!.status).toBe('ok'); + expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(tokenCountingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + + const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); + expect(modelsSpan).toBeDefined(); + expect(modelsSpan!.status).toBe('ok'); + expect(modelsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('models'); + expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + expect(modelsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(modelsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(modelsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(modelsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + + const streamingSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'msg_stream123', + ); + expect(streamingSpan).toBeDefined(); + expect(streamingSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(streamingSpan!.status).toBe('ok'); + expect(streamingSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); }, }) .expect({ event: EXPECTED_STREAM_EVENT_HANDLER_MESSAGE }) @@ -334,43 +351,53 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + const requestStreamSpans = container.items.filter( + span => span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(requestStreamSpans).toHaveLength(2); + for (const span of requestStreamSpans) { + expect(span.name).toBe('chat claude-3-haiku-20240307'); + expect(span.status).toBe('ok'); + expect(span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(span.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); + } + + const detailedStreamSpan = requestStreamSpans.find( + span => span.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]?.value === '["end_turn"]', + ); + expect(detailedStreamSpan).toBeDefined(); + expect(detailedStreamSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(detailedStreamSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(detailedStreamSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe( + 'claude-3-haiku-20240307', + ); + expect(detailedStreamSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe( + 'claude-3-haiku-20240307', + ); + expect(detailedStreamSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(detailedStreamSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(detailedStreamSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - // [0] messages.create with stream: true - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["end_turn"]'); - - // [1] messages.stream (no request.stream attribute — distinguishes from the other two) - expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - - // [2] messages.stream with redundant stream: true param - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); + const messagesStreamSpan = container.items.find( + span => span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(messagesStreamSpan).toBeDefined(); + expect(messagesStreamSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(messagesStreamSpan!.status).toBe('ok'); + expect(messagesStreamSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(messagesStreamSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(messagesStreamSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe( + 'claude-3-haiku-20240307', + ); + expect(messagesStreamSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(messagesStreamSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe( + 'claude-3-haiku-20240307', + ); + expect(messagesStreamSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('msg_stream_1'); + expect(messagesStreamSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(messagesStreamSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(messagesStreamSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); }, }) .start() @@ -386,28 +413,26 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] messages.create with stream: true — response text captured with PII - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); - - // [1] messages.stream — response text captured with PII, no request.stream param - expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); - - // [2] messages.stream with redundant stream: true — response text captured with PII - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + const requestStreamSpans = container.items.filter( + span => span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(requestStreamSpans).toHaveLength(2); + for (const span of requestStreamSpans) { + expect(span.name).toBe('chat claude-3-haiku-20240307'); + expect(span.status).toBe('ok'); + expect(span.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(span.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); + } + + const messagesStreamSpan = container.items.find( + span => span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(messagesStreamSpan).toBeDefined(); + expect(messagesStreamSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(messagesStreamSpan!.status).toBe('ok'); + expect(messagesStreamSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(messagesStreamSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('Hello from stream!'); }, }) .start() @@ -460,22 +485,23 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] messages.create(stream: true) with tools — available tools + tool calls recorded with PII - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe(EXPECTED_TOOLS_JSON); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toBe(EXPECTED_TOOL_CALLS_JSON); + const streamingToolSpan = container.items.find(span => span.status === 'ok'); + expect(streamingToolSpan).toBeDefined(); + expect(streamingToolSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(streamingToolSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(streamingToolSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(streamingToolSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(streamingToolSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe( + EXPECTED_TOOLS_JSON, + ); + expect(streamingToolSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toBe( + EXPECTED_TOOL_CALLS_JSON, + ); - // [1] messages.stream — currently records as error since messages.stream doesn't get - // iterable semantics through the mock; this preserves observed behavior. - expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + const errorSpan = container.items.find(span => span.status === 'error'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); }, }) .start() @@ -496,33 +522,43 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] messages.create(stream: true) error on stream init - expect(firstSpan!.name).toBe('chat error-stream-init'); - expect(firstSpan!.status).toBe('error'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-init'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - - // [1] messages.stream error on stream init (no request.stream param) - expect(secondSpan!.name).toBe('chat error-stream-init'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-init'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - - // [2] messages.create(stream: true) midway error — finishes 'ok' with partial text - expect(thirdSpan!.name).toBe('chat error-stream-midway'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-midway'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('This stream will '); - - // [3] messages.stream midway error — errors out - expect(fourthSpan!.name).toBe('chat error-stream-midway'); - expect(fourthSpan!.status).toBe('error'); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-stream-midway'); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + const createInitErrorSpan = container.items.find( + span => + span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]?.value === 'error-stream-init' && + span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(createInitErrorSpan).toBeDefined(); + expect(createInitErrorSpan!.name).toBe('chat error-stream-init'); + expect(createInitErrorSpan!.status).toBe('error'); + + const streamInitErrorSpan = container.items.find( + span => + span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]?.value === 'error-stream-init' && + span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(streamInitErrorSpan).toBeDefined(); + expect(streamInitErrorSpan!.name).toBe('chat error-stream-init'); + expect(streamInitErrorSpan!.status).toBe('error'); + + const createMidwayErrorSpan = container.items.find( + span => + span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]?.value === 'error-stream-midway' && + span.status === 'ok', + ); + expect(createMidwayErrorSpan).toBeDefined(); + expect(createMidwayErrorSpan!.name).toBe('chat error-stream-midway'); + expect(createMidwayErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE].value).toBe(true); + expect(createMidwayErrorSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(createMidwayErrorSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toBe('This stream will '); + + const streamMidwayErrorSpan = container.items.find( + span => + span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]?.value === 'error-stream-midway' && + span.status === 'error', + ); + expect(streamMidwayErrorSpan).toBeDefined(); + expect(streamMidwayErrorSpan!.name).toBe('chat error-stream-midway'); + expect(streamMidwayErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); }, }) .start() @@ -543,24 +579,22 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] messages.create with invalid-format — tool format error - expect(firstSpan!.name).toBe('chat invalid-format'); - expect(firstSpan!.status).toBe('error'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('invalid-format'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - - // [1] models.retrieve('nonexistent-model') — model retrieval error - expect(secondSpan!.name).toBe('models nonexistent-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('nonexistent-model'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - - // [2] Successful tool usage (for comparison) - expect(thirdSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toContain('tool_ok_1'); + const invalidFormatSpan = container.items.find(span => span.name === 'chat invalid-format'); + expect(invalidFormatSpan).toBeDefined(); + expect(invalidFormatSpan!.status).toBe('error'); + expect(invalidFormatSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('invalid-format'); + expect(invalidFormatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + + const modelErrorSpan = container.items.find(span => span.name === 'models nonexistent-model'); + expect(modelErrorSpan).toBeDefined(); + expect(modelErrorSpan!.status).toBe('error'); + expect(modelErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('nonexistent-model'); + expect(modelErrorSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); + + const toolSuccessSpan = container.items.find(span => span.name === 'chat claude-3-haiku-20240307'); + expect(toolSuccessSpan).toBeDefined(); + expect(toolSuccessSpan!.status).toBe('ok'); + expect(toolSuccessSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toContain('tool_ok_1'); }, }) .start() @@ -584,34 +618,38 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] First call: last message is large and truncated (only C's remain, D's cropped) - expect(firstSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, - ); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - - // [1] Second call: last message is small and kept without truncation const smallMsgValue = JSON.stringify([ { role: 'user', content: 'This is a small message that fits within the limit' }, ]); - expect(secondSpan!.name).toBe('chat claude-3-haiku-20240307'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(smallMsgValue); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + const truncatedSpan = container.items.find(span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.match( + /^\[\{"role":"user","content":"C+"\}\]$/, + ), + ); + expect(truncatedSpan).toBeDefined(); + expect(truncatedSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(truncatedSpan!.status).toBe('ok'); + expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(truncatedSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(truncatedSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(truncatedSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(truncatedSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-haiku-20240307'); + expect(truncatedSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + + const smallMessageSpan = container.items.find( + span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === smallMsgValue, + ); + expect(smallMessageSpan).toBeDefined(); + expect(smallMessageSpan!.name).toBe('chat claude-3-haiku-20240307'); + expect(smallMessageSpan!.status).toBe('ok'); + expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(smallMessageSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(smallMessageSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe( + 'claude-3-haiku-20240307', + ); + expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() @@ -720,15 +758,17 @@ describe('Anthropic integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] messages.create with multi-message conversation — all messages preserved - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedAllMessages); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + const conversationSpan = container.items.find( + span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedAllMessages, + ); + expect(conversationSpan).toBeDefined(); + expect(conversationSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - // [1] messages.create with long string input — not truncated - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedLongString); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); + const longStringSpan = container.items.find( + span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedLongString, + ); + expect(longStringSpan).toBeDefined(); + expect(longStringSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 508f52331bf1..f1e9cd333eee 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -36,41 +36,45 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] chat.sendMessage (should get model from context) - expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.google_genai'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); - - // [1] models.generateContent - expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-flash'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); - - // [2] error handling - expect(thirdSpan!.name).toBe('generate_content error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gemini-1.5-pro', + 'generate_content error-model', + 'generate_content gemini-1.5-flash', + ]); + + const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.status).toBe('ok'); + expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(chatSpan!.attributes['sentry.origin'].value).toBe('auto.ai.google_genai'); + expect(chatSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + expect(chatSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(generateContentSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-flash'); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + + const errorSpan = container.items.find(span => span.name === 'generate_content error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); }, }) .start() @@ -86,38 +90,42 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] chat.sendMessage with PII - expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); - - // [1] models.generateContent with PII - expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - - // [2] error handling with PII - expect(thirdSpan!.name).toBe('generate_content error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gemini-1.5-pro', + 'generate_content error-model', + 'generate_content gemini-1.5-flash', + ]); + + const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.status).toBe('ok'); + expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(chatSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(chatSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(generateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + + const errorSpan = container.items.find(span => span.name === 'generate_content error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -133,21 +141,25 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gemini-1.5-pro', + 'generate_content error-model', + 'generate_content gemini-1.5-flash', + ]); - // [0] chat.sendMessage with custom options (PII enabled via recordInputs/recordOutputs) - expect(firstSpan!.name).toBe('chat gemini-1.5-pro'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - // [1] models.generateContent with custom options - expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - // [2] error handling with custom options - expect(thirdSpan!.name).toBe('generate_content error-model'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + const errorSpan = container.items.find(span => span.name === 'generate_content error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); }, }) .start() @@ -166,50 +178,52 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] Non-streaming with tools - expect(firstSpan!.name).toBe('generate_content gemini-2.0-flash-001'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe( - EXPECTED_AVAILABLE_TOOLS_JSON, + const nonStreamingToolsSpan = container.items.find( + span => + span.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]?.value === EXPECTED_AVAILABLE_TOOLS_JSON && + span.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE] === undefined, + ); + expect(nonStreamingToolsSpan).toBeDefined(); + expect(nonStreamingToolsSpan!.name).toBe('generate_content gemini-2.0-flash-001'); + expect(nonStreamingToolsSpan!.status).toBe('ok'); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(nonStreamingToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(23); + + const streamingToolsSpan = container.items.find( + span => + span.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]?.value === EXPECTED_AVAILABLE_TOOLS_JSON && + span.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]?.value === true, ); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toBeUndefined(); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(23); - - // [1] Streaming with tools - expect(secondSpan!.name).toBe('generate_content gemini-2.0-flash-001'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toBe( - EXPECTED_AVAILABLE_TOOLS_JSON, + expect(streamingToolsSpan).toBeDefined(); + expect(streamingToolsSpan!.name).toBe('generate_content gemini-2.0-flash-001'); + expect(streamingToolsSpan!.status).toBe('ok'); + expect(streamingToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(streamingToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(streamingToolsSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(streamingToolsSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); + expect(streamingToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-tools-id'); + expect(streamingToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-2.0-flash-001'); + expect(streamingToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(streamingToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(streamingToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(22); + + const noToolsSpan = container.items.find( + span => span.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] === undefined, ); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-tools-id'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-2.0-flash-001'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(22); - - // [2] Without tools for comparison - expect(thirdSpan!.name).toBe('generate_content gemini-2.0-flash-001'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(thirdSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + expect(noToolsSpan).toBeDefined(); + expect(noToolsSpan!.name).toBe('generate_content gemini-2.0-flash-001'); + expect(noToolsSpan!.status).toBe('ok'); + expect(noToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(noToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(noToolsSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(noToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(noToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(noToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); }, }) .start() @@ -225,42 +239,49 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] models.generateContentStream (streaming) - expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-streaming-id'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(22); - - // [1] chat.sendMessageStream (streaming) - expect(secondSpan!.name).toBe('chat gemini-1.5-pro'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-streaming-id'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); - - // [2] blocked content streaming - expect(thirdSpan!.name).toBe('generate_content blocked-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - - // [3] error handling for streaming - expect(fourthSpan!.name).toBe('generate_content error-model'); - expect(fourthSpan!.status).toBe('error'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gemini-1.5-pro', + 'generate_content blocked-model', + 'generate_content error-model', + 'generate_content gemini-1.5-flash', + ]); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe( + 'mock-response-streaming-id', + ); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(generateContentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(22); + + const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.status).toBe('ok'); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE].value).toBe('mock-response-streaming-id'); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gemini-1.5-pro'); + + const blockedSpan = container.items.find(span => span.name === 'generate_content blocked-model'); + expect(blockedSpan).toBeDefined(); + expect(blockedSpan!.status).toBe('error'); + expect(blockedSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(blockedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + + const errorSpan = container.items.find(span => span.name === 'generate_content error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); }, }) .start() @@ -276,41 +297,46 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] models.generateContentStream (streaming) with PII - expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); - - // [1] chat.sendMessageStream (streaming) with PII - expect(secondSpan!.name).toBe('chat gemini-1.5-pro'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); - - // [2] blocked content stream with PII - expect(thirdSpan!.name).toBe('generate_content blocked-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - - // [3] error handling for streaming with PII - expect(fourthSpan!.name).toBe('generate_content error-model'); - expect(fourthSpan!.status).toBe('error'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat gemini-1.5-pro', + 'generate_content blocked-model', + 'generate_content error-model', + 'generate_content gemini-1.5-flash', + ]); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(generateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.9); + expect(generateContentSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(generateContentSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); + + const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.status).toBe('ok'); + expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(chatSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["STOP"]'); + + const blockedSpan = container.items.find(span => span.name === 'generate_content blocked-model'); + expect(blockedSpan).toBeDefined(); + expect(blockedSpan!.status).toBe('error'); + expect(blockedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(blockedSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE].value).toBe(true); + expect(blockedSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(blockedSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + + const errorSpan = container.items.find(span => span.name === 'generate_content error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -330,30 +356,32 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] First call: Last message is large and gets truncated (only C's remain, D's are cropped) - expect(firstSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","parts":\[\{"text":"C+"\}\]\}\]$/, + const truncatedSpan = container.items.find(span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.match( + /^\[\{"role":"user","parts":\[\{"text":"C+"\}\]\}\]$/, + ), ); - - // [1] Second call: Last message is small and kept without truncation - expect(secondSpan!.name).toBe('generate_content gemini-1.5-flash'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([ - { - role: 'user', - parts: [{ text: 'This is a small message that fits within the limit' }], - }, - ]), + expect(truncatedSpan).toBeDefined(); + expect(truncatedSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(truncatedSpan!.status).toBe('ok'); + expect(truncatedSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(truncatedSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + + const smallMessageSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + JSON.stringify([ + { + role: 'user', + parts: [{ text: 'This is a small message that fits within the limit' }], + }, + ]), ); + expect(smallMessageSpan).toBeDefined(); + expect(smallMessageSpan!.name).toBe('generate_content gemini-1.5-flash'); + expect(smallMessageSpan!.status).toBe('ok'); + expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() @@ -398,29 +426,30 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embedContent with string contents (no PII) - expect(firstSpan!.name).toBe('embeddings text-embedding-004'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.google_genai'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('text-embedding-004'); - expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toBeUndefined(); - - // [1] embedContent error model - expect(secondSpan!.name).toBe('embeddings error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); - - // [2] embedContent with array contents (no PII) - expect(thirdSpan!.name).toBe('embeddings text-embedding-004'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'embeddings error-model', + 'embeddings text-embedding-004', + 'embeddings text-embedding-004', + ]); + + const successfulSpans = container.items.filter( + span => span.name === 'embeddings text-embedding-004' && span.status === 'ok', + ); + expect(successfulSpans).toHaveLength(2); + for (const span of successfulSpans) { + expect(span.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(span.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(span.attributes['sentry.origin'].value).toBe('auto.ai.google_genai'); + expect(span.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + expect(span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('text-embedding-004'); + expect(span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toBeUndefined(); + } + + const errorSpan = container.items.find(span => span.name === 'embeddings error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); }, }) .start() @@ -436,30 +465,38 @@ describe('Google GenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embedContent with string contents and PII - expect(firstSpan!.name).toBe('embeddings text-embedding-004'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); - expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( - 'What is the capital of France?', + expect(container.items.map(span => span.name).sort()).toEqual([ + 'embeddings error-model', + 'embeddings text-embedding-004', + 'embeddings text-embedding-004', + ]); + + const stringInputSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'What is the capital of France?', ); - - // [1] embedContent error model with PII - expect(secondSpan!.name).toBe('embeddings error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); - expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('This will fail'); - - // [2] embedContent with array contents and PII - expect(thirdSpan!.name).toBe('embeddings text-embedding-004'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); - expect(thirdSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( - '[{"role":"user","parts":[{"text":"First input text"}]},{"role":"user","parts":[{"text":"Second input text"}]}]', + expect(stringInputSpan).toBeDefined(); + expect(stringInputSpan!.name).toBe('embeddings text-embedding-004'); + expect(stringInputSpan!.status).toBe('ok'); + expect(stringInputSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(stringInputSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('google_genai'); + + const errorSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'This will fail', + ); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.name).toBe('embeddings error-model'); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + + const arrayInputSpan = container.items.find( + span => + span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === + '[{"role":"user","parts":[{"text":"First input text"}]},{"role":"user","parts":[{"text":"Second input text"}]}]', ); + expect(arrayInputSpan).toBeDefined(); + expect(arrayInputSpan!.name).toBe('embeddings text-embedding-004'); + expect(arrayInputSpan!.status).toBe('ok'); + expect(arrayInputSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts index 53135efea5cb..5ba5a2b20b5f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/test.ts @@ -36,46 +36,50 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] chat model with claude-3-5-sonnet - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - - // [1] chat model with claude-3-opus - expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - - // [2] error handling - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan).toBeDefined(); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(sonnetSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + + const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan).toBeDefined(); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); }, }) .start() @@ -92,16 +96,9 @@ describe('LangChain integration', () => { // Without the dedup guard, the file-level and module-level hooks // both patch the same prototype, producing 6 spans instead of 3. expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] chat claude-3-5-sonnet - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - - // [1] chat claude-3-opus - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - - // [2] chat error-model - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + for (const span of container.items) { + expect(span.attributes['sentry.op'].value).toBe('gen_ai.chat'); + } }, }) .start() @@ -117,46 +114,50 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] chat model with PII - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - - // [1] chat model with PII - expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - - // [2] error handling with PII - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); + + const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan).toBeDefined(); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan).toBeDefined(); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -207,30 +208,35 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] String input truncated (only C's remain, D's are cropped) - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, + const stringInputSpan = container.items.find( + span => span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 1, ); - - // [1] Array input, last message truncated (only C's remain, D's are cropped) - expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + expect(stringInputSpan).toBeDefined(); + expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( /^\[\{"role":"user","content":"C+"\}\]$/, ); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); - // [2] Last message is small and kept without truncation - expect(thirdSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), + const arrayInputSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 2 && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.match( + /^\[\{"role":"user","content":"C+"\}\]$/, + ), ); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); + expect(arrayInputSpan).toBeDefined(); + expect(arrayInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(arrayInputSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); + + const smallMessageSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), + ); + expect(smallMessageSpan).toBeDefined(); + expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); + expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -251,17 +257,18 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // Verify the edge case limitation: - // [0] Direct Anthropic call made BEFORE LangChain import — IS instrumented - // by Anthropic (origin: 'auto.ai.anthropic'). - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + const anthropicSpan = container.items.find( + span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic', + ); + expect(anthropicSpan).toBeDefined(); + expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - // [1] LangChain call — IS instrumented by LangChain (origin: 'auto.ai.langchain'). - expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + // LangChain call is instrumented by LangChain. + const langchainSpan = container.items.find( + span => span.attributes['sentry.origin'].value === 'auto.ai.langchain', + ); + expect(langchainSpan).toBeDefined(); + expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // Third call (not present): Direct Anthropic call made AFTER LangChain import // is NOT instrumented, which demonstrates the skip mechanism works for NEW @@ -312,28 +319,33 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] format_prompt chain (invoke_agent) - expect(firstSpan!.name).toBe('chain format_prompt'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes['langchain.chain.name'].value).toBe('format_prompt'); - - // [1] chat model invoked inside the chain - expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - - // [2] parse_output chain (invoke_agent) - expect(thirdSpan!.name).toBe('chain parse_output'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(thirdSpan!.attributes['langchain.chain.name'].value).toBe('parse_output'); - - // [3] unknown_chain (fallback name) - expect(fourthSpan!.name).toBe('chain unknown_chain'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chain format_prompt', + 'chain parse_output', + 'chain unknown_chain', + 'chat claude-3-5-sonnet-20241022', + ]); + + const formatPromptSpan = container.items.find(span => span.name === 'chain format_prompt'); + expect(formatPromptSpan).toBeDefined(); + expect(formatPromptSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(formatPromptSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(formatPromptSpan!.attributes['langchain.chain.name'].value).toBe('format_prompt'); + + const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(chatSpan).toBeDefined(); + expect(chatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(chatSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + + const parseOutputSpan = container.items.find(span => span.name === 'chain parse_output'); + expect(parseOutputSpan).toBeDefined(); + expect(parseOutputSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(parseOutputSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(parseOutputSpan!.attributes['langchain.chain.name'].value).toBe('parse_output'); + + const unknownChainSpan = container.items.find(span => span.name === 'chain unknown_chain'); + expect(unknownChainSpan).toBeDefined(); + expect(unknownChainSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); }, }) .start() @@ -353,28 +365,30 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embedQuery span - expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('text-embedding-3-small'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); - - // [1] embedDocuments span - expect(secondSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); - - // [2] Error span - expect(thirdSpan!.name).toBe('embeddings error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'embeddings error-model', + 'embeddings text-embedding-3-small', + 'embeddings text-embedding-3-small', + ]); + + const successfulSpans = container.items.filter( + span => span.name === 'embeddings text-embedding-3-small' && span.status === 'ok', + ); + expect(successfulSpans).toHaveLength(2); + for (const span of successfulSpans) { + expect(span.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + expect(span.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(span.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('embeddings'); + expect(span.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('text-embedding-3-small'); + expect(span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); + } + + const errorSpan = container.items.find(span => span.name === 'embeddings error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); }, }) .start() @@ -389,16 +403,9 @@ describe('LangChain integration', () => { span: container => { // The scenario makes 3 embedding calls (2 successful + 1 error). expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embedQuery - expect(firstSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); - - // [1] embedDocuments - expect(secondSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); - - // [2] error embedding call - expect(thirdSpan!.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + for (const span of container.items) { + expect(span.attributes['sentry.op'].value).toBe(GEN_AI_EMBEDDINGS_OPERATION_ATTRIBUTE); + } }, }) .start() @@ -414,24 +421,32 @@ describe('LangChain integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embedQuery span with input recorded - expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('Hello world'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); - - // [1] embedDocuments span with input recorded - expect(secondSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( - JSON.stringify(['First document', 'Second document']), + expect(container.items.map(span => span.name).sort()).toEqual([ + 'embeddings error-model', + 'embeddings text-embedding-3-small', + 'embeddings text-embedding-3-small', + ]); + + const querySpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'Hello world', + ); + expect(querySpan).toBeDefined(); + expect(querySpan!.name).toBe('embeddings text-embedding-3-small'); + expect(querySpan!.status).toBe('ok'); + expect(querySpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE].value).toBe(1536); + + const documentsSpan = container.items.find( + span => + span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === + JSON.stringify(['First document', 'Second document']), ); + expect(documentsSpan).toBeDefined(); + expect(documentsSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(documentsSpan!.status).toBe('ok'); - // [2] error embedding span (input still recorded with PII) - expect(thirdSpan!.name).toBe('embeddings error-model'); - expect(thirdSpan!.status).toBe('error'); + const errorSpan = container.items.find(span => span.name === 'embeddings error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts index 309d165fd674..af2a4c46775d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langchain/v1/test.ts @@ -40,46 +40,50 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); - // [0] chat model with claude-3-5-sonnet - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan).toBeDefined(); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(sonnetSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - // [1] chat model with claude-3-opus - expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan).toBeDefined(); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(opusSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(opusSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - // [2] error handling - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); }, }) .start() @@ -107,46 +111,50 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat claude-3-5-sonnet-20241022', + 'chat claude-3-opus-20240229', + 'chat error-model', + ]); - // [0] chat model with PII - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + const sonnetSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022'); + expect(sonnetSpan).toBeDefined(); + expect(sonnetSpan!.status).toBe('ok'); + expect(sonnetSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(sonnetSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(sonnetSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-5-sonnet-20241022'); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(sonnetSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(sonnetSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE]).toBeDefined(); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(sonnetSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - // [1] chat model with PII - expect(secondSpan!.name).toBe('chat claude-3-opus-20240229'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); + const opusSpan = container.items.find(span => span.name === 'chat claude-3-opus-20240229'); + expect(opusSpan).toBeDefined(); + expect(opusSpan!.status).toBe('ok'); + expect(opusSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('claude-3-opus-20240229'); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.9); + expect(opusSpan!.attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE].value).toBe(0.95); + expect(opusSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(200); + expect(opusSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toBeDefined(); + expect(opusSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(opusSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(opusSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(25); - // [2] error handling with PII - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('anthropic'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + expect(errorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -217,32 +225,37 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] String input truncated (only C's remain, D's are cropped) - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(1); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + const stringInputSpan = container.items.find( + span => span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 1, + ); + expect(stringInputSpan).toBeDefined(); + expect(stringInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(stringInputSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( /^\[\{"role":"user","content":"C+"\}\]$/, ); - // [1] Array input, last message truncated (only C's remain, D's are cropped) - expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( - /^\[\{"role":"user","content":"C+"\}\]$/, + const arrayInputSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]?.value === 2 && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.match( + /^\[\{"role":"user","content":"C+"\}\]$/, + ), ); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + expect(arrayInputSpan).toBeDefined(); + expect(arrayInputSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(arrayInputSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( /^\[\{"type":"text","content":"A+"\}\]$/, ); - // [2] Last message is small and kept without truncation - expect(thirdSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( - JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), + const smallMessageSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), ); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + expect(smallMessageSpan).toBeDefined(); + expect(smallMessageSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); + expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(2); + expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( /^\[\{"type":"text","content":"A+"\}\]$/, ); }, @@ -272,16 +285,17 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] Direct Anthropic call made BEFORE LangChain import — instrumented - // by Anthropic (origin: 'auto.ai.anthropic'). - expect(firstSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.anthropic'); + const anthropicSpan = container.items.find( + span => span.attributes['sentry.origin'].value === 'auto.ai.anthropic', + ); + expect(anthropicSpan).toBeDefined(); + expect(anthropicSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - // [1] LangChain call — instrumented by LangChain (origin: 'auto.ai.langchain'). - expect(secondSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + const langchainSpan = container.items.find( + span => span.attributes['sentry.origin'].value === 'auto.ai.langchain', + ); + expect(langchainSpan).toBeDefined(); + expect(langchainSpan!.name).toBe('chat claude-3-5-sonnet-20241022'); // Third call (not present): Direct Anthropic call made AFTER LangChain import // is NOT instrumented, demonstrating the skip mechanism works for NEW clients. @@ -313,46 +327,50 @@ conditionalTest({ min: 20 })('LangChain integration (v1)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + expect(container.items.map(span => span.name).sort()).toEqual([ + 'chat error-model', + 'chat gpt-3.5-turbo', + 'chat gpt-4o', + ]); - // [0] initChatModel with gpt-4o - expect(firstSpan!.name).toBe('chat gpt-4o'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gpt-4o'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4o'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + const gpt4oSpan = container.items.find(span => span.name === 'chat gpt-4o'); + expect(gpt4oSpan).toBeDefined(); + expect(gpt4oSpan!.status).toBe('ok'); + expect(gpt4oSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(gpt4oSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(gpt4oSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('chat'); + expect(gpt4oSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gpt-4o'); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.7); + expect(gpt4oSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE].value).toBe(100); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(gpt4oSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toBeDefined(); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4o'); + expect(gpt4oSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - // [1] initChatModel with gpt-3.5-turbo - expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gpt-3.5-turbo'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.5); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-3.5-turbo'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); + const gpt35Span = container.items.find(span => span.name === 'chat gpt-3.5-turbo'); + expect(gpt35Span).toBeDefined(); + expect(gpt35Span!.status).toBe('ok'); + expect(gpt35Span!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(gpt35Span!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(gpt35Span!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(gpt35Span!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('gpt-3.5-turbo'); + expect(gpt35Span!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE].value).toBe(0.5); + expect(gpt35Span!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(8); + expect(gpt35Span!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(12); + expect(gpt35Span!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(20); + expect(gpt35Span!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-3.5-turbo'); + expect(gpt35Span!.attributes[GEN_AI_RESPONSE_STOP_REASON_ATTRIBUTE].value).toBe('stop'); - // [2] error handling - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); + const errorSpan = container.items.find(span => span.name === 'chat error-model'); + expect(errorSpan).toBeDefined(); + expect(errorSpan!.status).toBe('error'); + expect(errorSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(errorSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langchain'); + expect(errorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('openai'); + expect(errorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('error-model'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts index 95b66e9524f7..2907c4b9e95e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts @@ -32,33 +32,30 @@ describe('LangGraph integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] create_agent - expect(firstSpan!.name).toBe('create_agent weather_assistant'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('create_agent'); - expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); - - // [1] first invoke_agent - expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); - expect(secondSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); - expect(secondSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('weather_assistant'); - - // [2] second invoke_agent - expect(thirdSpan!.name).toBe('invoke_agent weather_assistant'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); - expect(thirdSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + expect(container.items.map(span => span.name).sort()).toEqual([ + 'create_agent weather_assistant', + 'invoke_agent weather_assistant', + 'invoke_agent weather_assistant', + ]); + + const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant'); + expect(createAgentSpan).toBeDefined(); + expect(createAgentSpan!.status).toBe('ok'); + expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(createAgentSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('create_agent'); + expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + + const invokeAgentSpans = container.items.filter(span => span.name === 'invoke_agent weather_assistant'); + expect(invokeAgentSpans).toHaveLength(2); + for (const span of invokeAgentSpans) { + expect(span.status).toBe('ok'); + expect(span.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(span.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(span.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); + expect(span.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + expect(span.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('weather_assistant'); + } }, }) .start() @@ -74,27 +71,27 @@ describe('LangGraph integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] create_agent - expect(firstSpan!.name).toBe('create_agent weather_assistant'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - - // [1] first invoke_agent with PII ("What is the weather today?") - expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( - 'What is the weather today?', - ); + const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant'); + expect(createAgentSpan).toBeDefined(); + expect(createAgentSpan!.status).toBe('ok'); + expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - // [2] second invoke_agent with PII ("Tell me about the weather") - expect(thirdSpan!.name).toBe('invoke_agent weather_assistant'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('Tell me about the weather'); + const weatherTodaySpan = container.items.find(span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.includes('What is the weather today?'), + ); + expect(weatherTodaySpan).toBeDefined(); + expect(weatherTodaySpan!.name).toBe('invoke_agent weather_assistant'); + expect(weatherTodaySpan!.status).toBe('ok'); + expect(weatherTodaySpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(weatherTodaySpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + + const weatherDetailsSpan = container.items.find(span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Tell me about the weather'), + ); + expect(weatherDetailsSpan).toBeDefined(); + expect(weatherDetailsSpan!.name).toBe('invoke_agent weather_assistant'); + expect(weatherDetailsSpan!.status).toBe('ok'); + expect(weatherDetailsSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); }, }) .start() @@ -110,45 +107,56 @@ describe('LangGraph integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] create_agent tool_agent - expect(firstSpan!.name).toBe('create_agent tool_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('tool_agent'); - - // [1] invoke_agent tool_agent (tools available, not called) - expect(secondSpan!.name).toBe('invoke_agent tool_agent'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toContain('get_weather'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('What is the weather?'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4-0613'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toContain( + expect(container.items.map(span => span.name).sort()).toEqual([ + 'create_agent tool_agent', + 'create_agent tool_calling_agent', + 'invoke_agent tool_agent', + 'invoke_agent tool_calling_agent', + ]); + + const toolAgentSpan = container.items.find(span => span.name === 'create_agent tool_agent'); + expect(toolAgentSpan).toBeDefined(); + expect(toolAgentSpan!.status).toBe('ok'); + expect(toolAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(toolAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('tool_agent'); + + const toolAgentInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_agent'); + expect(toolAgentInvokeSpan).toBeDefined(); + expect(toolAgentInvokeSpan!.status).toBe('ok'); + expect(toolAgentInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toContain( + 'get_weather', + ); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'What is the weather?', + ); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4-0613'); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toContain( 'Response without calling tools', ); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(25); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(40); - - // [2] create_agent tool_calling_agent - expect(thirdSpan!.name).toBe('create_agent tool_calling_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(thirdSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('tool_calling_agent'); - - // [3] invoke_agent tool_calling_agent (with tool calls) - expect(fourthSpan!.name).toBe('invoke_agent tool_calling_agent'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('San Francisco'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4-0613'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toMatch(/"role":"tool"/); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toContain('get_weather'); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(80); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(40); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(120); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(25); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(toolAgentInvokeSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(40); + + const toolCallingAgentSpan = container.items.find(span => span.name === 'create_agent tool_calling_agent'); + expect(toolCallingAgentSpan).toBeDefined(); + expect(toolCallingAgentSpan!.status).toBe('ok'); + expect(toolCallingAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(toolCallingAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('tool_calling_agent'); + + const toolCallingInvokeSpan = container.items.find(span => span.name === 'invoke_agent tool_calling_agent'); + expect(toolCallingInvokeSpan).toBeDefined(); + expect(toolCallingInvokeSpan!.status).toBe('ok'); + expect(toolCallingInvokeSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain('San Francisco'); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('gpt-4-0613'); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE].value).toMatch(/"role":"tool"/); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE].value).toContain( + 'get_weather', + ); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(80); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(40); + expect(toolCallingInvokeSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(120); }, }) .start() @@ -165,28 +173,33 @@ describe('LangGraph integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] create_agent - expect(firstSpan!.name).toBe('create_agent thread_test_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - - // [1] first invoke_agent with thread_abc123_session_1 - expect(secondSpan!.name).toBe('invoke_agent thread_test_agent'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(secondSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE].value).toBe('thread_abc123_session_1'); - - // [2] second invoke_agent with thread_xyz789_session_2 - expect(thirdSpan!.name).toBe('invoke_agent thread_test_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE].value).toBe('thread_xyz789_session_2'); - - // [3] third invoke_agent without thread_id - expect(fourthSpan!.name).toBe('invoke_agent thread_test_agent'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); + const createAgentSpan = container.items.find(span => span.name === 'create_agent thread_test_agent'); + expect(createAgentSpan).toBeDefined(); + expect(createAgentSpan!.status).toBe('ok'); + expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + + const firstThreadSpan = container.items.find( + span => span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]?.value === 'thread_abc123_session_1', + ); + expect(firstThreadSpan).toBeDefined(); + expect(firstThreadSpan!.name).toBe('invoke_agent thread_test_agent'); + expect(firstThreadSpan!.status).toBe('ok'); + expect(firstThreadSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + const secondThreadSpan = container.items.find( + span => span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]?.value === 'thread_xyz789_session_2', + ); + expect(secondThreadSpan).toBeDefined(); + expect(secondThreadSpan!.name).toBe('invoke_agent thread_test_agent'); + expect(secondThreadSpan!.status).toBe('ok'); + + const noThreadSpan = container.items.find( + span => + span.name === 'invoke_agent thread_test_agent' && + span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] === undefined, + ); + expect(noThreadSpan).toBeDefined(); + expect(noThreadSpan!.status).toBe('ok'); }, }) .start() @@ -206,11 +219,10 @@ describe('LangGraph integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [, secondSpan] = container.items; + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent test-agent'); - // [1] invoke_agent with system instructions - expect(secondSpan!.name).toBe('invoke_agent test-agent'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), ); }, @@ -239,22 +251,22 @@ describe('LangGraph integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan] = container.items; - - // [0] create_agent resume_agent - expect(firstSpan!.name).toBe('create_agent resume_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); - expect(firstSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('resume_agent'); - - // [1] first invoke_agent with thread_id 'resume-thread-1' - expect(secondSpan!.name).toBe('invoke_agent resume_agent'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); - expect(secondSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('resume_agent'); - expect(secondSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('resume_agent'); - expect(secondSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE].value).toBe('resume-thread-1'); + const createAgentSpan = container.items.find(span => span.name === 'create_agent resume_agent'); + expect(createAgentSpan).toBeDefined(); + expect(createAgentSpan!.status).toBe('ok'); + expect(createAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.create_agent'); + expect(createAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('resume_agent'); + + const invokeAgentSpan = container.items.find( + span => span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]?.value === 'resume-thread-1', + ); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent resume_agent'); + expect(invokeAgentSpan!.status).toBe('ok'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.origin'].value).toBe('auto.ai.langgraph'); + expect(invokeAgentSpan!.attributes[GEN_AI_AGENT_NAME_ATTRIBUTE].value).toBe('resume_agent'); + expect(invokeAgentSpan!.attributes[GEN_AI_PIPELINE_NAME_ATTRIBUTE].value).toBe('resume_agent'); }, }) .start() @@ -282,12 +294,13 @@ describe('LangGraph integration', () => { ]); expect(container.items).toHaveLength(2); - const [, secondSpan] = container.items; + const invokeAgentSpan = container.items.find( + span => span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === expectedMessages, + ); - // [1] invoke_agent with untruncated input - expect(secondSpan!.name).toBe('invoke_agent weather_assistant'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe(expectedMessages); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent weather_assistant'); + expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts index a300a0a79192..f4185bfc66f8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/openai-tool-calls/test.ts @@ -84,148 +84,239 @@ describe('OpenAI Tool Calls integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] chat completion with tools (non-streaming) - expect(firstSpan!.name).toBe('chat gpt-4'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const chatToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-tools-123', + ); + expect(chatToolsSpan).toBeDefined(); + expect(chatToolsSpan!.name).toBe('chat gpt-4'); + expect(chatToolsSpan!.status).toBe('ok'); + expect(chatToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(chatToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(chatToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-tools-123', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["tool_calls"]', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + expect(chatToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(chatToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); + expect(chatToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 40, + }); - // [1] chat completion with tools and streaming - expect(secondSpan!.name).toBe('chat gpt-4'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingChatToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-tools-123', + ); + expect(streamingChatToolsSpan).toBeDefined(); + expect(streamingChatToolsSpan!.name).toBe('chat gpt-4'); + expect(streamingChatToolsSpan!.status).toBe('ok'); + expect(streamingChatToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-stream-tools-123', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["tool_calls"]', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 40, + }); - // [2] responses API with tools (non-streaming) - expect(thirdSpan!.name).toBe('chat gpt-4'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const responsesToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_tools_789', + ); + expect(responsesToolsSpan).toBeDefined(); + expect(responsesToolsSpan!.name).toBe('chat gpt-4'); + expect(responsesToolsSpan!.status).toBe('ok'); + expect(responsesToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_tools_789', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["completed"]', }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(responsesToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(responsesToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(responsesToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 20, + }); - // [3] responses API with tools and streaming - expect(fourthSpan!.name).toBe('chat gpt-4'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingResponsesToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_stream_tools_789', + ); + expect(streamingResponsesToolsSpan).toBeDefined(); + expect(streamingResponsesToolsSpan!.name).toBe('chat gpt-4'); + expect(streamingResponsesToolsSpan!.status).toBe('ok'); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_stream_tools_789', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["in_progress","completed"]', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 20, + }); }, }) .start() @@ -241,197 +332,291 @@ describe('OpenAI Tool Calls integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] chat completion with tools (non-streaming) with PII - expect(firstSpan!.name).toBe('chat gpt-4'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const chatToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-tools-123', + ); + expect(chatToolsSpan).toBeDefined(); + expect(chatToolsSpan!.name).toBe('chat gpt-4'); + expect(chatToolsSpan!.status).toBe('ok'); + expect(chatToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(chatToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(chatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-tools-123', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["tool_calls"]', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: '[""]' }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + type: 'string', + value: '[""]', + }); + expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ type: 'string', value: CHAT_TOOL_CALLS, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + expect(chatToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(chatToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); + expect(chatToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 40, + }); - // [1] chat completion with tools and streaming with PII - expect(secondSpan!.name).toBe('chat gpt-4'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingChatToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-tools-123', + ); + expect(streamingChatToolsSpan).toBeDefined(); + expect(streamingChatToolsSpan!.name).toBe('chat gpt-4'); + expect(streamingChatToolsSpan!.status).toBe('ok'); + expect(streamingChatToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-stream-tools-123', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["tool_calls"]', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ type: 'string', value: CHAT_STREAM_TOOL_CALLS, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 40 }); + expect(streamingChatToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 40, + }); - // [2] responses API with tools (non-streaming) with PII - expect(thirdSpan!.name).toBe('chat gpt-4'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const responsesToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_tools_789', + ); + expect(responsesToolsSpan).toBeDefined(); + expect(responsesToolsSpan!.name).toBe('chat gpt-4'); + expect(responsesToolsSpan!.status).toBe('ok'); + expect(responsesToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(responsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_tools_789', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["completed"]', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ type: 'string', value: RESPONSES_TOOL_CALLS, }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(responsesToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(responsesToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(responsesToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 20, + }); - // [3] responses API with tools and streaming with PII - expect(fourthSpan!.name).toBe('chat gpt-4'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingResponsesToolsSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_stream_tools_789', + ); + expect(streamingResponsesToolsSpan).toBeDefined(); + expect(streamingResponsesToolsSpan!.name).toBe('chat gpt-4'); + expect(streamingResponsesToolsSpan!.status).toBe('ok'); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the weather like in Paris today?"}]', }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toEqual({ type: 'string', value: WEATHER_TOOL_DEFINITION, }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_stream_tools_789', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["in_progress","completed"]', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]).toEqual({ type: 'string', value: RESPONSES_TOOL_CALLS, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 }); + expect(streamingResponsesToolsSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 20, + }); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts index 3e4de45d3a81..2d5d1daa68ce 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -37,180 +37,288 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(6); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; - - // [0] basic chat completion without PII - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(chatCompletionSpan!.status).toBe('ok'); + expect(chatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.7, + }); + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-mock123', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); - // [1] responses API - expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const responsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', + ); + expect(responsesSpan).toBeDefined(); + expect(responsesSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(responsesSpan!.status).toBe('ok'); + expect(responsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(responsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_mock456', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["completed"]', }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 5 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 13 }); + expect(responsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 5, + }); + expect(responsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(responsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 13, + }); - // [2] error handling (non-streaming) - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const nonStreamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(nonStreamingErrorSpan).toBeDefined(); + expect(nonStreamingErrorSpan!.name).toBe('chat error-model'); + expect(nonStreamingErrorSpan!.status).toBe('error'); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - // [3] chat completions streaming - expect(fourthSpan!.name).toBe('chat gpt-4'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingChatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-123', + ); + expect(streamingChatCompletionSpan).toBeDefined(); + expect(streamingChatCompletionSpan!.name).toBe('chat gpt-4'); + expect(streamingChatCompletionSpan!.status).toBe('ok'); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.8, }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-stream-123', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 18, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30 }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 30, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [4] responses API streaming - expect(fifthSpan!.name).toBe('chat gpt-4'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingResponsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_stream_456', + ); + expect(streamingResponsesSpan).toBeDefined(); + expect(streamingResponsesSpan!.name).toBe('chat gpt-4'); + expect(streamingResponsesSpan!.status).toBe('ok'); + expect(streamingResponsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_stream_456', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["in_progress","completed"]', }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 6 }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 16 }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 6, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 16, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [5] error handling in streaming context - expect(sixthSpan!.name).toBe('chat error-model'); - expect(sixthSpan!.status).toBe('error'); - expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const streamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(streamingErrorSpan).toBeDefined(); + expect(streamingErrorSpan!.name).toBe('chat error-model'); + expect(streamingErrorSpan!.status).toBe('error'); + expect(streamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -229,252 +337,360 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(6); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; - - // [0] basic chat completion with PII - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(chatCompletionSpan!.status).toBe('ok'); + expect(chatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7 }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + type: 'double', + value: 0.7, + }); + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the capital of France?"}]', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ type: 'string', value: JSON.stringify([{ type: 'text', content: 'You are a helpful assistant.' }]), }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-mock123', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: '["Hello from OpenAI mock!"]', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 }); + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 15, + }); + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 25, + }); - // [1] responses API with PII - expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const responsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', + ); + expect(responsesSpan).toBeDefined(); + expect(responsesSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(responsesSpan!.status).toBe('ok'); + expect(responsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(responsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Translate this to French: Hello', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Response to: Translate this to French: Hello', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["completed"]', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_mock456', }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 5 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8 }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 13 }); + expect(responsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 5, + }); + expect(responsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 8, + }); + expect(responsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 13, + }); - // [2] error handling with PII (non-streaming) - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const nonStreamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(nonStreamingErrorSpan).toBeDefined(); + expect(nonStreamingErrorSpan!.name).toBe('chat error-model'); + expect(nonStreamingErrorSpan!.status).toBe('error'); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', }); - // [3] chat completions streaming with PII - expect(fourthSpan!.name).toBe('chat gpt-4'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingChatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-123', + ); + expect(streamingChatCompletionSpan).toBeDefined(); + expect(streamingChatCompletionSpan!.name).toBe('chat gpt-4'); + expect(streamingChatCompletionSpan!.status).toBe('ok'); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.8, }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"Tell me about streaming"}]', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ type: 'string', value: JSON.stringify([{ type: 'text', content: 'You are a helpful assistant.' }]), }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Hello from OpenAI streaming!', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-stream-123', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 12, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 18, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30 }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 30, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [4] responses API streaming with PII - expect(fifthSpan!.name).toBe('chat gpt-4'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingResponsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_stream_456', + ); + expect(streamingResponsesSpan).toBeDefined(); + expect(streamingResponsesSpan!.name).toBe('chat gpt-4'); + expect(streamingResponsesSpan!.status).toBe('ok'); + expect(streamingResponsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Test streaming responses API', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Streaming response to: Test streaming responses APITest streaming responses API', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["in_progress","completed"]', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_stream_456', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 6 }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 16 }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 6, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 16, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [5] error handling in streaming context with PII - expect(sixthSpan!.name).toBe('chat error-model'); - expect(sixthSpan!.status).toBe('error'); - expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const streamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(streamingErrorSpan).toBeDefined(); + expect(streamingErrorSpan!.name).toBe('chat error-model'); + expect(streamingErrorSpan!.status).toBe('error'); + expect(streamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', }); - expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -493,26 +709,33 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(6); - const [firstSpan, , , fourthSpan] = container.items; - - // [0] non-streaming with input messages recorded via custom options - expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - // [3] streaming with input messages recorded via custom options - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + const streamingChatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-123', + ); + expect(streamingChatCompletionSpan).toBeDefined(); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); @@ -541,14 +764,15 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] chat completions: multiple messages all preserved (no popping to last message only) - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-mock123', }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: JSON.stringify([ { role: 'user', content: longContent }, @@ -556,21 +780,24 @@ describe('OpenAI integration', () => { { role: 'user', content: 'Follow-up question' }, ]), }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ type: 'integer', value: 3, }); - // [1] responses API long string input is not truncated or wrapped in quotes - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + const responsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', + ); + expect(responsesSpan).toBeDefined(); + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_mock456', }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: 'B'.repeat(50_000), }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ type: 'integer', value: 1, }); @@ -594,90 +821,120 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embeddings API (single input) - expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const singleEmbeddingSpan = container.items.find( + span => + span.name === 'embeddings text-embedding-3-small' && + span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE] !== undefined, + ); + expect(singleEmbeddingSpan).toBeDefined(); + expect(singleEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(singleEmbeddingSpan!.status).toBe('ok'); + expect(singleEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ type: 'string', value: 'float', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ type: 'integer', value: 1536, }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); - // [1] embeddings API error model - expect(secondSpan!.name).toBe('embeddings error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const errorEmbeddingSpan = container.items.find(span => span.name === 'embeddings error-model'); + expect(errorEmbeddingSpan).toBeDefined(); + expect(errorEmbeddingSpan!.name).toBe('embeddings error-model'); + expect(errorEmbeddingSpan!.status).toBe('error'); + expect(errorEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(errorEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(errorEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - // [2] embeddings API (multiple inputs) - expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const multiEmbeddingSpan = container.items.find( + span => + span.name === 'embeddings text-embedding-3-small' && + span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE] === undefined, + ); + expect(multiEmbeddingSpan).toBeDefined(); + expect(multiEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(multiEmbeddingSpan!.status).toBe('ok'); + expect(multiEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); }, }) .start() @@ -697,102 +954,132 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embeddings API with PII (single input) - expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const singleEmbeddingSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'Embedding test!', + ); + expect(singleEmbeddingSpan).toBeDefined(); + expect(singleEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(singleEmbeddingSpan!.status).toBe('ok'); + expect(singleEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ type: 'string', value: 'float', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ type: 'integer', value: 1536, }); - expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Embedding test!', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); - // [1] embeddings API error model with PII - expect(secondSpan!.name).toBe('embeddings error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const errorEmbeddingSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'Error embedding test!', + ); + expect(errorEmbeddingSpan).toBeDefined(); + expect(errorEmbeddingSpan!.name).toBe('embeddings error-model'); + expect(errorEmbeddingSpan!.status).toBe('error'); + expect(errorEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(errorEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(errorEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + expect(errorEmbeddingSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Error embedding test!', }); - // [2] embeddings API with multiple inputs (not truncated) - expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const multiEmbeddingSpan = container.items.find( + span => + span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === + '["First input text","Second input text","Third input text"]', + ); + expect(multiEmbeddingSpan).toBeDefined(); + expect(multiEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(multiEmbeddingSpan!.status).toBe('ok'); + expect(multiEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ type: 'string', value: '["First input text","Second input text","Third input text"]', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10 }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + type: 'integer', + value: 10, + }); }, }) .start() @@ -900,67 +1187,84 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] Last message is large and gets truncated (only C's remain, D's are cropped) - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const truncatedMessageSpan = container.items.find(span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.match( + /^\[\{"role":"user","content":"C+"\}\]$/, + ), + ); + expect(truncatedMessageSpan).toBeDefined(); + expect(truncatedMessageSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(truncatedMessageSpan!.status).toBe('ok'); + expect(truncatedMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(truncatedMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(truncatedMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(truncatedMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(truncatedMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(truncatedMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 2, }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + expect(truncatedMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( /^\[\{"role":"user","content":"C+"\}\]$/, ); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + expect(truncatedMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( /^\[\{"type":"text","content":"A+"\}\]$/, ); - // [1] Last message is small and kept without truncation - expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const smallMessageSpan = container.items.find( + span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + JSON.stringify([{ role: 'user', content: 'This is a small message that fits within the limit' }]), + ); + expect(smallMessageSpan).toBeDefined(); + expect(smallMessageSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(smallMessageSpan!.status).toBe('ok'); + expect(smallMessageSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(smallMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(smallMessageSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(smallMessageSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: JSON.stringify([ { role: 'user', content: 'This is a small message that fits within the limit' }, ]), }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(smallMessageSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 2, }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( + expect(smallMessageSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toMatch( /^\[\{"type":"text","content":"A+"\}\]$/, ); }, @@ -1032,66 +1336,103 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] conversations.create returns conversation object with id - expect(firstSpan!.name).toBe('chat unknown'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const conversationCreateSpan = container.items.find(span => span.name === 'chat unknown'); + expect(conversationCreateSpan).toBeDefined(); + expect(conversationCreateSpan!.name).toBe('chat unknown'); + expect(conversationCreateSpan!.status).toBe('ok'); + expect(conversationCreateSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(conversationCreateSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(conversationCreateSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ + expect(conversationCreateSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(conversationCreateSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'conv_689667905b048191b4740501625afd940c7533ace33a2dab', }); - // [1] responses.create with conversation parameter - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const conversationResponseSpan = container.items.find( + span => + span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]?.value === + 'conv_689667905b048191b4740501625afd940c7533ace33a2dab' && + span.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]?.value === 'gpt-4', + ); + expect(conversationResponseSpan).toBeDefined(); + expect(conversationResponseSpan!.status).toBe('ok'); + expect(conversationResponseSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(conversationResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(conversationResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(secondSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ + expect(conversationResponseSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(conversationResponseSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(conversationResponseSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'conv_689667905b048191b4740501625afd940c7533ace33a2dab', }); - // [2] responses.create without conversation (first in chain, should NOT have gen_ai.conversation.id) - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const unlinkedResponseSpan = container.items.find( + span => + span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]?.value === 'gen_ai.chat' && + span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE] === undefined, + ); + expect(unlinkedResponseSpan).toBeDefined(); + expect(unlinkedResponseSpan!.status).toBe('ok'); + expect(unlinkedResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); + expect(unlinkedResponseSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toBeUndefined(); - // [3] responses.create with previous_response_id (chaining) - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const previousResponseSpan = container.items.find( + span => span.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]?.value === 'resp_mock_conv_123', + ); + expect(previousResponseSpan).toBeDefined(); + expect(previousResponseSpan!.status).toBe('ok'); + expect(previousResponseSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(previousResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(previousResponseSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fourthSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ + expect(previousResponseSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(previousResponseSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(previousResponseSpan!.attributes[GEN_AI_CONVERSATION_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_mock_conv_123', }); @@ -1115,10 +1456,9 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; // All three chat completion spans should have the same manually-set conversation ID - for (const span of [firstSpan, secondSpan, thirdSpan]) { + for (const span of container.items) { expect(span!.name).toBe('chat gpt-4'); expect(span!.status).toBe('ok'); expect(span!.attributes['gen_ai.conversation.id']).toEqual({ @@ -1149,10 +1489,9 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; // Both chat completion spans should have the expected conversation ID - for (const span of [firstSpan, secondSpan]) { + for (const span of container.items) { expect(span!.name).toBe('chat gpt-4'); expect(span!.status).toBe('ok'); expect(span!.attributes['gen_ai.conversation.id']).toEqual({ @@ -1182,10 +1521,9 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; // Both chat completion spans should have the expected conversation ID - for (const span of [firstSpan, secondSpan]) { + for (const span of container.items) { expect(span!.name).toBe('chat gpt-4'); expect(span!.status).toBe('ok'); expect(span!.attributes['gen_ai.conversation.id']).toEqual({ @@ -1246,10 +1584,9 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; // Both calls should produce spans with the same response ID - for (const span of [firstSpan, secondSpan]) { + for (const span of container.items) { expect(span!.name).toBe('chat gpt-4'); expect(span!.status).toBe('ok'); expect(span!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); @@ -1278,10 +1615,9 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; // Both vision request spans should contain [Blob substitute] - for (const span of [firstSpan, secondSpan]) { + for (const span of container.items) { expect(span!.name).toBe('chat gpt-4o'); expect(span!.status).toBe('ok'); expect(span!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); @@ -1309,10 +1645,11 @@ describe('OpenAI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [, secondSpan] = container.items; - - // [1] multiple images span contains the https URL - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( + const multipleImagesSpan = container.items.find(span => + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.includes('https://example.com/image.png'), + ); + expect(multipleImagesSpan).toBeDefined(); + expect(multipleImagesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( 'https://example.com/image.png', ); }, diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts index 906cb5ee61c4..f19302122622 100644 --- a/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/openai/v6/test.ts @@ -40,231 +40,288 @@ describe('OpenAI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(6); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; - - // [0] basic chat completion without PII - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(chatCompletionSpan!.status).toBe('ok'); + expect(chatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7, }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-mock123', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25, }); - // [1] responses API - expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const responsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', + ); + expect(responsesSpan).toBeDefined(); + expect(responsesSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(responsesSpan!.status).toBe('ok'); + expect(responsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(responsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_mock456', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["completed"]', }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 5, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 13, }); - // [2] error handling (non-streaming) - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const nonStreamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(nonStreamingErrorSpan).toBeDefined(); + expect(nonStreamingErrorSpan!.name).toBe('chat error-model'); + expect(nonStreamingErrorSpan!.status).toBe('error'); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - // [3] chat completions streaming - expect(fourthSpan!.name).toBe('chat gpt-4'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const streamingChatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-123', + ); + expect(streamingChatCompletionSpan).toBeDefined(); + expect(streamingChatCompletionSpan!.name).toBe('chat gpt-4'); + expect(streamingChatCompletionSpan!.status).toBe('ok'); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4', }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.8, }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-stream-123', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 18, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30, }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [4] responses API streaming - expect(fifthSpan!.name).toBe('chat gpt-4'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingResponsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_stream_456', + ); + expect(streamingResponsesSpan).toBeDefined(); + expect(streamingResponsesSpan!.name).toBe('chat gpt-4'); + expect(streamingResponsesSpan!.status).toBe('ok'); + expect(streamingResponsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_stream_456', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["in_progress","completed"]', }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 6, }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 16, }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [5] error handling in streaming context - expect(sixthSpan!.name).toBe('chat error-model'); - expect(sixthSpan!.status).toBe('error'); - expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const streamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(streamingErrorSpan).toBeDefined(); + expect(streamingErrorSpan!.name).toBe('chat error-model'); + expect(streamingErrorSpan!.status).toBe('error'); + expect(streamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -293,303 +350,360 @@ describe('OpenAI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(6); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan] = container.items; - - // [0] basic chat completion with PII - expect(firstSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(chatCompletionSpan!.status).toBe('ok'); + expect(chatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(chatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.7, }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"What is the capital of France?"}]', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"type":"text","content":"You are a helpful assistant."}]', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-mock123', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: '["Hello from OpenAI mock!"]', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(chatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25, }); - // [1] responses API with PII - expect(secondSpan!.name).toBe('chat gpt-3.5-turbo'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const responsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_mock456', + ); + expect(responsesSpan).toBeDefined(); + expect(responsesSpan!.name).toBe('chat gpt-3.5-turbo'); + expect(responsesSpan!.status).toBe('ok'); + expect(responsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(responsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); + expect(responsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Translate this to French: Hello', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Response to: Translate this to French: Hello', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["completed"]', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-3.5-turbo', }); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_mock456', }); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 5, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 8, }); - expect(secondSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(responsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 13, }); - // [2] error handling with PII (non-streaming) - expect(thirdSpan!.name).toBe('chat error-model'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const nonStreamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] === undefined, + ); + expect(nonStreamingErrorSpan).toBeDefined(); + expect(nonStreamingErrorSpan!.name).toBe('chat error-model'); + expect(nonStreamingErrorSpan!.status).toBe('error'); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(nonStreamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(nonStreamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', }); - // [3] chat completions streaming with PII - expect(fourthSpan!.name).toBe('chat gpt-4'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const streamingChatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-123', + ); + expect(streamingChatCompletionSpan).toBeDefined(); + expect(streamingChatCompletionSpan!.name).toBe('chat gpt-4'); + expect(streamingChatCompletionSpan!.status).toBe('ok'); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fourthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4', }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]).toEqual({ type: 'double', value: 0.8, }); - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"Tell me about streaming"}]', }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"type":"text","content":"You are a helpful assistant."}]', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Hello from OpenAI streaming!', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["stop"]', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'chatcmpl-stream-123', }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4', }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 18, }); - expect(fourthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 30, }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [4] responses API streaming with PII - expect(fifthSpan!.name).toBe('chat gpt-4'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + const streamingResponsesSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'resp_stream_456', + ); + expect(streamingResponsesSpan).toBeDefined(); + expect(streamingResponsesSpan!.name).toBe('chat gpt-4'); + expect(streamingResponsesSpan!.status).toBe('ok'); + expect(streamingResponsesSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(fifthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingResponsesSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(fifthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4' }); - expect(fifthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'gpt-4', + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: 'Test streaming responses API', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Streaming response to: Test streaming responses APITest streaming responses API', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]).toEqual({ type: 'string', value: '["in_progress","completed"]', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]).toEqual({ type: 'string', value: 'resp_stream_456', }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'gpt-4', }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 6, }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 16, }); - expect(fifthSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ + expect(streamingResponsesSpan!.attributes[GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]).toEqual({ type: 'boolean', value: true, }); - // [5] error handling in streaming context with PII - expect(sixthSpan!.name).toBe('chat error-model'); - expect(sixthSpan!.status).toBe('error'); - expect(sixthSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + const streamingErrorSpan = container.items.find( + span => + span.name === 'chat error-model' && span.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]?.value === true, + ); + expect(streamingErrorSpan).toBeDefined(); + expect(streamingErrorSpan!.name).toBe('chat error-model'); + expect(streamingErrorSpan!.status).toBe('error'); + expect(streamingErrorSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'chat', + }); + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toEqual({ type: 'integer', value: 1, }); - expect(sixthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toEqual({ type: 'string', value: '[{"role":"user","content":"This will fail"}]', }); - expect(sixthSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(streamingErrorSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.chat', }); - expect(sixthSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(streamingErrorSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); @@ -618,42 +732,51 @@ describe('OpenAI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(6); - const [firstSpan, , , fourthSpan] = container.items; - - // [0] non-streaming with input messages recorded via custom options - expect(firstSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + const chatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-mock123', + ); + expect(chatCompletionSpan).toBeDefined(); + expect(chatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined(); + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ type: 'integer', value: 1, }); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + expect(chatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toMatchObject({ + expect(chatCompletionSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + expect(chatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - // [3] streaming with input messages recorded via custom options - expect(fourthSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ type: 'boolean', value: true }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]).toMatchObject({ + const streamingChatCompletionSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_ID_ATTRIBUTE]?.value === 'chatcmpl-stream-123', + ); + expect(streamingChatCompletionSpan).toBeDefined(); + expect(streamingChatCompletionSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toEqual({ + type: 'boolean', + value: true, + }); + expect( + streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE], + ).toMatchObject({ type: 'integer', value: 1, }); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - expect(fourthSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toMatchObject({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ + expect(streamingChatCompletionSpan!.attributes[GEN_AI_RESPONSE_TEXT_ATTRIBUTE]).toMatchObject({ type: 'string', value: expect.any(String), }); @@ -686,99 +809,117 @@ describe('OpenAI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embeddings API (single input) - expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const singleEmbeddingSpan = container.items.find( + span => + span.name === 'embeddings text-embedding-3-small' && + span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE] !== undefined, + ); + expect(singleEmbeddingSpan).toBeDefined(); + expect(singleEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(singleEmbeddingSpan!.status).toBe('ok'); + expect(singleEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ type: 'string', value: 'float', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ type: 'integer', value: 1536, }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - // [1] embeddings API error model - expect(secondSpan!.name).toBe('embeddings error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const errorEmbeddingSpan = container.items.find(span => span.name === 'embeddings error-model'); + expect(errorEmbeddingSpan).toBeDefined(); + expect(errorEmbeddingSpan!.name).toBe('embeddings error-model'); + expect(errorEmbeddingSpan!.status).toBe('error'); + expect(errorEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(errorEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(errorEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - // [2] embeddings API (multiple inputs) - expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const multiEmbeddingSpan = container.items.find( + span => + span.name === 'embeddings text-embedding-3-small' && + span.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE] === undefined, + ); + expect(multiEmbeddingSpan).toBeDefined(); + expect(multiEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(multiEmbeddingSpan!.status).toBe('ok'); + expect(multiEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); @@ -811,111 +952,129 @@ describe('OpenAI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] embeddings API with PII (single input) - expect(firstSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const singleEmbeddingSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'Embedding test!', + ); + expect(singleEmbeddingSpan).toBeDefined(); + expect(singleEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(singleEmbeddingSpan!.status).toBe('ok'); + expect(singleEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(firstSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(singleEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_ENCODING_FORMAT_ATTRIBUTE]).toEqual({ type: 'string', value: 'float', }); - expect(firstSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE]).toEqual({ type: 'integer', value: 1536, }); - expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Embedding test!', }); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(singleEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - // [1] embeddings API error model with PII - expect(secondSpan!.name).toBe('embeddings error-model'); - expect(secondSpan!.status).toBe('error'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const errorEmbeddingSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'Error embedding test!', + ); + expect(errorEmbeddingSpan).toBeDefined(); + expect(errorEmbeddingSpan!.name).toBe('embeddings error-model'); + expect(errorEmbeddingSpan!.status).toBe('error'); + expect(errorEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(secondSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(errorEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(secondSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(errorEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(errorEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'error-model', }); - expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + expect(errorEmbeddingSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ type: 'string', value: 'Error embedding test!', }); - // [2] embeddings API with multiple inputs (not truncated) - expect(thirdSpan!.name).toBe('embeddings text-embedding-3-small'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ + const multiEmbeddingSpan = container.items.find( + span => + span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === + '["First input text","Second input text","Third input text"]', + ); + expect(multiEmbeddingSpan).toBeDefined(); + expect(multiEmbeddingSpan!.name).toBe('embeddings text-embedding-3-small'); + expect(multiEmbeddingSpan!.status).toBe('ok'); + expect(multiEmbeddingSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toEqual({ type: 'string', value: 'gen_ai.embeddings', }); - expect(thirdSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ + expect(multiEmbeddingSpan!.attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.ai.openai', }); - expect(thirdSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ type: 'string', value: 'openai' }); - expect(thirdSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE]).toEqual({ + type: 'string', + value: 'openai', + }); + expect(multiEmbeddingSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]).toEqual({ type: 'string', value: '["First input text","Second input text","Third input text"]', }); - expect(thirdSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]).toEqual({ type: 'string', value: 'text-embedding-3-small', }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); - expect(thirdSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ + expect(multiEmbeddingSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 10, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index e3091415e18d..3001a07765b2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -35,68 +35,104 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; - - // [0] First generateText — invoke_agent (no explicit telemetry, no PII) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); - - // [1] First generateText — generate_content (doGenerate, no PII) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); - - // [2] Second generateText — invoke_agent (explicit telemetry enabled) - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const firstInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes['vercel.ai.operationId'].value === 'ai.generateText' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] === undefined && + span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value === 10, + ); + expect(firstInvokeAgentSpan).toBeDefined(); + expect(firstInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(firstInvokeAgentSpan!.status).toBe('ok'); + expect(firstInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstInvokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + const firstGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes['vercel.ai.operationId'].value === 'ai.generateText.doGenerate' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] === undefined, + ); + expect(firstGenerateContentSpan).toBeDefined(); + expect(firstGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(firstGenerateContentSpan!.status).toBe('ok'); + expect(firstGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(firstGenerateContentSpan!.attributes['vercel.ai.operationId'].value).toBe( + 'ai.generateText.doGenerate', + ); + expect(firstGenerateContentSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); + expect(firstGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + const secondInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(secondInvokeAgentSpan).toBeDefined(); + expect(secondInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(secondInvokeAgentSpan!.status).toBe('ok'); + expect(secondInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the second span?"}]', ); - expect(thirdSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + expect(secondInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', ); - // [3] Second generateText — generate_content (doGenerate with telemetry) - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(fourthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(fourthSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('Second span here!'); - - // [4] Third generateText — invoke_agent (tool call) - expect(fifthSpan!.name).toBe('invoke_agent'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(fifthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(fifthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(25); - expect(fifthSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(40); - - // [5] Third generateText — generate_content (doGenerate with tools) - expect(sixthSpan!.name).toBe('generate_content mock-model-id'); - expect(sixthSpan!.status).toBe('ok'); - expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - - // [6] Tool execution - expect(seventhSpan!.name).toBe('execute_tool getWeather'); - expect(seventhSpan!.status).toBe('ok'); - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + const secondGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Second span here!'), + ); + expect(secondGenerateContentSpan).toBeDefined(); + expect(secondGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(secondGenerateContentSpan!.status).toBe('ok'); + expect(secondGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(secondGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(secondGenerateContentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'Second span here!', + ); + + const toolInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 15, + ); + expect(toolInvokeAgentSpan).toBeDefined(); + expect(toolInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(toolInvokeAgentSpan!.status).toBe('ok'); + expect(toolInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(toolInvokeAgentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(toolInvokeAgentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(25); + expect(toolInvokeAgentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(40); + + const toolGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 15, + ); + expect(toolGenerateContentSpan).toBeDefined(); + expect(toolGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolGenerateContentSpan!.status).toBe('ok'); + expect(toolGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(toolGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + const toolExecutionSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolExecutionSpan).toBeDefined(); + expect(toolExecutionSpan!.name).toBe('execute_tool getWeather'); + expect(toolExecutionSpan!.status).toBe('ok'); + expect(toolExecutionSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); }, }) .start() @@ -111,67 +147,105 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; - - // [0] First generateText — invoke_agent (PII auto-enabled) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const firstInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the first span?"}]', + ); + expect(firstInvokeAgentSpan).toBeDefined(); + expect(firstInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(firstInvokeAgentSpan!.status).toBe('ok'); + expect(firstInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstInvokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the first span?"}]', ); - expect(firstSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + expect(firstInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', ); - // [1] First doGenerate with PII - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('First span here!'); + const firstGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('First span here!'), + ); + expect(firstGenerateContentSpan).toBeDefined(); + expect(firstGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(firstGenerateContentSpan!.status).toBe('ok'); + expect(firstGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(firstGenerateContentSpan!.attributes['vercel.ai.operationId'].value).toBe( + 'ai.generateText.doGenerate', + ); + expect(firstGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstGenerateContentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'First span here!', + ); - // [2] Second generateText — invoke_agent (explicit telemetry) - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const secondInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(secondInvokeAgentSpan).toBeDefined(); + expect(secondInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(secondInvokeAgentSpan!.status).toBe('ok'); + expect(secondInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the second span?"}]', ); - // [3] Second doGenerate - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - - // [4] Third generateText — invoke_agent (tool call prompt) - expect(fifthSpan!.name).toBe('invoke_agent'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const secondGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Second span here!'), + ); + expect(secondGenerateContentSpan).toBeDefined(); + expect(secondGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(secondGenerateContentSpan!.status).toBe('ok'); + expect(secondGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + const toolInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"What is the weather in San Francisco?"}]', + ); + expect(toolInvokeAgentSpan).toBeDefined(); + expect(toolInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(toolInvokeAgentSpan!.status).toBe('ok'); + expect(toolInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(toolInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"What is the weather in San Francisco?"}]', ); - expect(fifthSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - - // [5] Third doGenerate with available tools - expect(sixthSpan!.name).toBe('generate_content mock-model-id'); - expect(sixthSpan!.status).toBe('ok'); - expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toContain('getWeather'); - expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - - // [6] Tool execution with PII - expect(seventhSpan!.name).toBe('execute_tool getWeather'); - expect(seventhSpan!.status).toBe('ok'); - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( + expect(toolInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + + const toolGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]?.value?.includes('getWeather'), + ); + expect(toolGenerateContentSpan).toBeDefined(); + expect(toolGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolGenerateContentSpan!.status).toBe('ok'); + expect(toolGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(toolGenerateContentSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE].value).toContain( + 'getWeather', + ); + expect(toolGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + const toolExecutionSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolExecutionSpan).toBeDefined(); + expect(toolExecutionSpan!.name).toBe('execute_tool getWeather'); + expect(toolExecutionSpan!.status).toBe('ok'); + expect(toolExecutionSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( 'Get the current weather for a location', ); - expect(seventhSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); - expect(seventhSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -197,25 +271,28 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] invoke_agent (errored due to tool error) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('error'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - - // [1] generate_content (doGenerate, succeeded) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - - // [2] execute_tool (errored) - expect(thirdSpan!.name).toBe('execute_tool getWeather'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + const invokeAgentSpan = container.items.find( + span => span.name === 'invoke_agent' && span.status === 'error', + ); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.status).toBe('error'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(generateContentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); + + const toolSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolSpan).toBeDefined(); + expect(toolSpan!.name).toBe('execute_tool getWeather'); + expect(toolSpan!.status).toBe('error'); + expect(toolSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); }, }) .expect({ @@ -252,24 +329,27 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] invoke_agent (errored) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('error'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - - // [1] generate_content (doGenerate, succeeded) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - - // [2] execute_tool (errored) - expect(thirdSpan!.name).toBe('execute_tool getWeather'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + const invokeAgentSpan = container.items.find( + span => span.name === 'invoke_agent' && span.status === 'error', + ); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.status).toBe('error'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + const toolSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolSpan).toBeDefined(); + expect(toolSpan!.name).toBe('execute_tool getWeather'); + expect(toolSpan!.status).toBe('error'); + expect(toolSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); }, }) .expect({ @@ -308,21 +388,21 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] invoke_agent - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); - expect(firstSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); - - // [1] generate_content (doGenerate) - expect(secondSpan!.name).toBe('generateText.doGenerate'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); - expect(secondSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.status).toBe('ok'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); + expect(invokeAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('invoke_agent'); + + const generateContentSpan = container.items.find(span => span.name === 'generateText.doGenerate'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generateText.doGenerate'); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(generateContentSpan!.attributes['sentry.origin'].value).toBe('auto.vercelai.otel'); + expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE].value).toBe('generate_content'); }, }) .start() @@ -342,18 +422,18 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] invoke_agent (carries system instructions) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE].value).toBe( JSON.stringify([{ type: 'text', content: 'You are a helpful assistant' }]), ); - // [1] generate_content - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); }, }) .start() @@ -374,21 +454,35 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, , thirdSpan] = container.items; - - // [0] First call — invoke_agent: last message truncated (only C's remain, D's cropped) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( + const truncatedInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.match(/^\[.*"(?:text|content)":"C+".*\]$/), + ); + expect(truncatedInvokeAgentSpan).toBeDefined(); + expect(truncatedInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(truncatedInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(truncatedInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe( + 3, + ); + expect(truncatedInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toMatch( /^\[.*"(?:text|content)":"C+".*\]$/, ); - // [2] Second call — invoke_agent: last message is small and kept intact - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( + const smallMessageInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value?.includes( + 'This is a small message that fits within the limit', + ), + ); + expect(smallMessageInvokeAgentSpan).toBeDefined(); + expect(smallMessageInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(smallMessageInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect( + smallMessageInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value, + ).toBe(3); + expect(smallMessageInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toContain( 'This is a small message that fits within the limit', ); }, @@ -406,20 +500,24 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] embed doEmbed - expect(firstSpan!.name).toBe('embeddings mock-model-id'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - - // [1] embedMany doEmbed - expect(secondSpan!.name).toBe('embeddings mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(20); + const embedSpan = container.items.find( + span => span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 10, + ); + expect(embedSpan).toBeDefined(); + expect(embedSpan!.name).toBe('embeddings mock-model-id'); + expect(embedSpan!.status).toBe('ok'); + expect(embedSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(embedSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(embedSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + + const embedManySpan = container.items.find( + span => span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 20, + ); + expect(embedManySpan).toBeDefined(); + expect(embedManySpan!.name).toBe('embeddings mock-model-id'); + expect(embedManySpan!.status).toBe('ok'); + expect(embedManySpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(embedManySpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(20); }, }) .start() @@ -434,19 +532,23 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] embed doEmbed with input - expect(firstSpan!.name).toBe('embeddings mock-model-id'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(firstSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('Embedding test!'); - - // [1] embedMany doEmbed with input - expect(secondSpan!.name).toBe('embeddings mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); - expect(secondSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( + const embedSpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === 'Embedding test!', + ); + expect(embedSpan).toBeDefined(); + expect(embedSpan!.name).toBe('embeddings mock-model-id'); + expect(embedSpan!.status).toBe('ok'); + expect(embedSpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(embedSpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe('Embedding test!'); + + const embedManySpan = container.items.find( + span => span.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]?.value === '["First input","Second input"]', + ); + expect(embedManySpan).toBeDefined(); + expect(embedManySpan!.name).toBe('embeddings mock-model-id'); + expect(embedManySpan!.status).toBe('ok'); + expect(embedManySpan!.attributes['sentry.op'].value).toBe('gen_ai.embeddings'); + expect(embedManySpan!.attributes[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE].value).toBe( '["First input","Second input"]', ); }, @@ -463,17 +565,17 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] invoke_agent with user-set conversation id - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['gen_ai.conversation.id'].value).toBe('conv-a'); - - // [1] generate_content also inherits the conversation id - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['gen_ai.conversation.id'].value).toBe('conv-a'); + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes['gen_ai.conversation.id'].value).toBe('conv-a'); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(generateContentSpan!.attributes['gen_ai.conversation.id'].value).toBe('conv-a'); }, }) .start() @@ -494,23 +596,32 @@ describe('Vercel AI integration', () => { .expect({ span: container => { expect(container.items).toHaveLength(2); - const [firstSpan, secondSpan] = container.items; - - // [0] invoke_agent — input messages preserved in full (no truncation) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const invokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + JSON.stringify([ + { role: 'user', content: longContent }, + { role: 'assistant', content: 'Some reply' }, + { role: 'user', content: 'Follow-up question' }, + ]), + ); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( JSON.stringify([ { role: 'user', content: longContent }, { role: 'assistant', content: 'Some reply' }, { role: 'user', content: 'Follow-up question' }, ]), ); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); + expect(invokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE].value).toBe(3); - // [1] generate_content - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts index 3fc6dd19f1ba..515f6cdc8c6e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts @@ -35,60 +35,94 @@ describe('Vercel AI integration (V5)', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; - - // [0] First generateText — invoke_agent (no explicit telemetry, no PII) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + const firstInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes['vercel.ai.operationId'].value === 'ai.generateText' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] === undefined && + span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value === 10, + ); + expect(firstInvokeAgentSpan).toBeDefined(); + expect(firstInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(firstInvokeAgentSpan!.status).toBe('ok'); + expect(firstInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstInvokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); - // [1] First generateText — generate_content (doGenerate, no PII) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + const firstGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes['vercel.ai.operationId'].value === 'ai.generateText.doGenerate' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] === undefined, + ); + expect(firstGenerateContentSpan).toBeDefined(); + expect(firstGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(firstGenerateContentSpan!.status).toBe('ok'); + expect(firstGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(firstGenerateContentSpan!.attributes['vercel.ai.operationId'].value).toBe( + 'ai.generateText.doGenerate', + ); + expect(firstGenerateContentSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); + expect(firstGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); - // [2] Second generateText — invoke_agent (explicit telemetry enabled) - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const secondInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(secondInvokeAgentSpan).toBeDefined(); + expect(secondInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(secondInvokeAgentSpan!.status).toBe('ok'); + expect(secondInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the second span?"}]', ); - expect(thirdSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + expect(secondInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', ); - // [3] Second generateText — generate_content (doGenerate with PII) - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const secondGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Second span here!'), + ); + expect(secondGenerateContentSpan).toBeDefined(); + expect(secondGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(secondGenerateContentSpan!.status).toBe('ok'); + expect(secondGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - // [4] Third generateText — invoke_agent (with tool call, no PII) - expect(fifthSpan!.name).toBe('invoke_agent'); - expect(fifthSpan!.status).toBe('ok'); + const toolInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 15, + ); + expect(toolInvokeAgentSpan).toBeDefined(); + expect(toolInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(toolInvokeAgentSpan!.status).toBe('ok'); - // [5] Third generateText — generate_content (doGenerate) - expect(sixthSpan!.name).toBe('generate_content mock-model-id'); - expect(sixthSpan!.status).toBe('ok'); + const toolGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 15, + ); + expect(toolGenerateContentSpan).toBeDefined(); + expect(toolGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolGenerateContentSpan!.status).toBe('ok'); - // [6] Tool execution - expect(seventhSpan!.name).toBe('execute_tool getWeather'); - expect(seventhSpan!.status).toBe('ok'); - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + const toolExecutionSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolExecutionSpan).toBeDefined(); + expect(toolExecutionSpan!.name).toBe('execute_tool getWeather'); + expect(toolExecutionSpan!.status).toBe('ok'); + expect(toolExecutionSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); }, }) .start() @@ -113,65 +147,101 @@ describe('Vercel AI integration (V5)', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; - - // [0] First generateText — invoke_agent (PII auto-enabled) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const firstInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the first span?"}]', + ); + expect(firstInvokeAgentSpan).toBeDefined(); + expect(firstInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(firstInvokeAgentSpan!.status).toBe('ok'); + expect(firstInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstInvokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the first span?"}]', ); - expect(firstSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + expect(firstInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', ); - // [1] First doGenerate with PII - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('First span here!'); + const firstGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('First span here!'), + ); + expect(firstGenerateContentSpan).toBeDefined(); + expect(firstGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(firstGenerateContentSpan!.status).toBe('ok'); + expect(firstGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(firstGenerateContentSpan!.attributes['vercel.ai.operationId'].value).toBe( + 'ai.generateText.doGenerate', + ); + expect(firstGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstGenerateContentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'First span here!', + ); - // [2] Second generateText — invoke_agent (explicit telemetry) - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const secondInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(secondInvokeAgentSpan).toBeDefined(); + expect(secondInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(secondInvokeAgentSpan!.status).toBe('ok'); + expect(secondInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the second span?"}]', ); - // [3] Second doGenerate - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const secondGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Second span here!'), + ); + expect(secondGenerateContentSpan).toBeDefined(); + expect(secondGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(secondGenerateContentSpan!.status).toBe('ok'); + expect(secondGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - // [4] Third generateText — invoke_agent (tool call prompt) - expect(fifthSpan!.name).toBe('invoke_agent'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const toolInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"What is the weather in San Francisco?"}]', + ); + expect(toolInvokeAgentSpan).toBeDefined(); + expect(toolInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(toolInvokeAgentSpan!.status).toBe('ok'); + expect(toolInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"What is the weather in San Francisco?"}]', ); - // [5] Third doGenerate with available tools - expect(sixthSpan!.name).toBe('generate_content mock-model-id'); - expect(sixthSpan!.status).toBe('ok'); - expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeDefined(); - expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + const toolGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] !== undefined, + ); + expect(toolGenerateContentSpan).toBeDefined(); + expect(toolGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolGenerateContentSpan!.status).toBe('ok'); + expect(toolGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(toolGenerateContentSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeDefined(); + expect(toolGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - // [6] Tool execution with PII - expect(seventhSpan!.name).toBe('execute_tool getWeather'); - expect(seventhSpan!.status).toBe('ok'); - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( + const toolExecutionSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolExecutionSpan).toBeDefined(); + expect(toolExecutionSpan!.name).toBe('execute_tool getWeather'); + expect(toolExecutionSpan!.status).toBe('ok'); + expect(toolExecutionSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( 'Get the current weather for a location', ); - expect(seventhSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); - expect(seventhSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -203,22 +273,23 @@ describe('Vercel AI integration (V5)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - // [0] invoke_agent - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - // [1] generate_content (doGenerate) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - - // [2] execute_tool (errored) - expect(thirdSpan!.name).toBe('execute_tool getWeather'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + const toolSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolSpan).toBeDefined(); + expect(toolSpan!.name).toBe('execute_tool getWeather'); + expect(toolSpan!.status).toBe('error'); + expect(toolSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); }, }) .expect({ @@ -263,18 +334,20 @@ describe('Vercel AI integration (V5)', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, , , fifthSpan, sixthSpan, seventhSpan] = container.items; - - // invoke_agent spans at [0], [2], [4] - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + const invokeAgentSpans = container.items.filter( + span => span.attributes['sentry.op'].value === 'gen_ai.invoke_agent', + ); + expect(invokeAgentSpans).toHaveLength(3); - // generate_content spans at [1], [3], [5] - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const generateContentSpans = container.items.filter( + span => span.attributes['sentry.op'].value === 'gen_ai.generate_content', + ); + expect(generateContentSpans).toHaveLength(3); - // execute_tool at [6] - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + const toolSpan = container.items.find( + span => span.attributes['sentry.op'].value === 'gen_ai.execute_tool', + ); + expect(toolSpan).toBeDefined(); }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts index 9c4e0ccd005e..037294a69b6b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts @@ -36,60 +36,94 @@ describe('Vercel AI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; - - // [0] First generateText — invoke_agent (no explicit telemetry, no PII) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - expect(firstSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(firstSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); - expect(firstSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); - - // [1] First generateText — generate_content (doGenerate, no PII) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - expect(secondSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); - - // [2] Second generateText — invoke_agent (explicit telemetry enabled) - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(thirdSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const firstInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes['vercel.ai.operationId'].value === 'ai.generateText' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] === undefined && + span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value === 10, + ); + expect(firstInvokeAgentSpan).toBeDefined(); + expect(firstInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(firstInvokeAgentSpan!.status).toBe('ok'); + expect(firstInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstInvokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE].value).toBe(30); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + const firstGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes['vercel.ai.operationId'].value === 'ai.generateText.doGenerate' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE] === undefined, + ); + expect(firstGenerateContentSpan).toBeDefined(); + expect(firstGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(firstGenerateContentSpan!.status).toBe('ok'); + expect(firstGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(firstGenerateContentSpan!.attributes['vercel.ai.operationId'].value).toBe( + 'ai.generateText.doGenerate', + ); + expect(firstGenerateContentSpan!.attributes[GEN_AI_SYSTEM_ATTRIBUTE].value).toBe('mock-provider'); + expect(firstGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(firstGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeUndefined(); + + const secondInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(secondInvokeAgentSpan).toBeDefined(); + expect(secondInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(secondInvokeAgentSpan!.status).toBe('ok'); + expect(secondInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(secondInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the second span?"}]', ); - expect(thirdSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + expect(secondInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', ); - // [3] Second generateText — generate_content (doGenerate with PII) - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - - // [4] Third generateText — invoke_agent (with tool call, no PII) - expect(fifthSpan!.name).toBe('invoke_agent'); - expect(fifthSpan!.status).toBe('ok'); - - // [5] Third generateText — generate_content (doGenerate) - expect(sixthSpan!.name).toBe('generate_content mock-model-id'); - expect(sixthSpan!.status).toBe('ok'); - - // [6] Tool execution - expect(seventhSpan!.name).toBe('execute_tool getWeather'); - expect(seventhSpan!.status).toBe('ok'); - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + const secondGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Second span here!'), + ); + expect(secondGenerateContentSpan).toBeDefined(); + expect(secondGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(secondGenerateContentSpan!.status).toBe('ok'); + expect(secondGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + const toolInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 15, + ); + expect(toolInvokeAgentSpan).toBeDefined(); + expect(toolInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(toolInvokeAgentSpan!.status).toBe('ok'); + + const toolGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]?.value === 15, + ); + expect(toolGenerateContentSpan).toBeDefined(); + expect(toolGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolGenerateContentSpan!.status).toBe('ok'); + + const toolExecutionSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolExecutionSpan).toBeDefined(); + expect(toolExecutionSpan!.name).toBe('execute_tool getWeather'); + expect(toolExecutionSpan!.status).toBe('ok'); + expect(toolExecutionSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); }, }) .start() @@ -114,62 +148,98 @@ describe('Vercel AI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, thirdSpan, fourthSpan, fifthSpan, sixthSpan, seventhSpan] = container.items; - - // [0] First generateText — invoke_agent (PII auto-enabled) - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); - expect(firstSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const firstInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the first span?"}]', + ); + expect(firstInvokeAgentSpan).toBeDefined(); + expect(firstInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(firstInvokeAgentSpan!.status).toBe('ok'); + expect(firstInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(firstInvokeAgentSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText'); + expect(firstInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"Where is the first span?"}]', ); - expect(firstSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( + expect(firstInvokeAgentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', ); - // [1] First doGenerate with PII - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes['vercel.ai.operationId'].value).toBe('ai.generateText.doGenerate'); - expect(secondSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); - expect(secondSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain('First span here!'); - - // [2] Second generateText — invoke_agent (explicit telemetry) - expect(thirdSpan!.name).toBe('invoke_agent'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - - // [3] Second doGenerate - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const firstGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('First span here!'), + ); + expect(firstGenerateContentSpan).toBeDefined(); + expect(firstGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(firstGenerateContentSpan!.status).toBe('ok'); + expect(firstGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(firstGenerateContentSpan!.attributes['vercel.ai.operationId'].value).toBe( + 'ai.generateText.doGenerate', + ); + expect(firstGenerateContentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]).toBeDefined(); + expect(firstGenerateContentSpan!.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE].value).toContain( + 'First span here!', + ); - // [4] Third generateText — invoke_agent (tool call prompt) - expect(fifthSpan!.name).toBe('invoke_agent'); - expect(fifthSpan!.status).toBe('ok'); - expect(fifthSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( + const secondInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"Where is the second span?"}]', + ); + expect(secondInvokeAgentSpan).toBeDefined(); + expect(secondInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(secondInvokeAgentSpan!.status).toBe('ok'); + expect(secondInvokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + const secondGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]?.value?.includes('Second span here!'), + ); + expect(secondGenerateContentSpan).toBeDefined(); + expect(secondGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(secondGenerateContentSpan!.status).toBe('ok'); + expect(secondGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + const toolInvokeAgentSpan = container.items.find( + span => + span.name === 'invoke_agent' && + span.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]?.value === + '[{"role":"user","content":"What is the weather in San Francisco?"}]', + ); + expect(toolInvokeAgentSpan).toBeDefined(); + expect(toolInvokeAgentSpan!.name).toBe('invoke_agent'); + expect(toolInvokeAgentSpan!.status).toBe('ok'); + expect(toolInvokeAgentSpan!.attributes[GEN_AI_INPUT_MESSAGES_ATTRIBUTE].value).toBe( '[{"role":"user","content":"What is the weather in San Francisco?"}]', ); - // [5] Third doGenerate with available tools - expect(sixthSpan!.name).toBe('generate_content mock-model-id'); - expect(sixthSpan!.status).toBe('ok'); - expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(sixthSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeDefined(); - expect(sixthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - - // [6] Tool execution with PII - expect(seventhSpan!.name).toBe('execute_tool getWeather'); - expect(seventhSpan!.status).toBe('ok'); - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(seventhSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( + const toolGenerateContentSpan = container.items.find( + span => + span.name === 'generate_content mock-model-id' && + span.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] !== undefined, + ); + expect(toolGenerateContentSpan).toBeDefined(); + expect(toolGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolGenerateContentSpan!.status).toBe('ok'); + expect(toolGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(toolGenerateContentSpan!.attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]).toBeDefined(); + expect(toolGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + + const toolExecutionSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolExecutionSpan).toBeDefined(); + expect(toolExecutionSpan!.name).toBe('execute_tool getWeather'); + expect(toolExecutionSpan!.status).toBe('ok'); + expect(toolExecutionSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE].value).toBe( 'Get the current weather for a location', ); - expect(seventhSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); - expect(seventhSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_INPUT_ATTRIBUTE]).toBeDefined(); + expect(toolExecutionSpan!.attributes[GEN_AI_TOOL_OUTPUT_ATTRIBUTE]).toBeDefined(); }, }) .start() @@ -201,22 +271,23 @@ describe('Vercel AI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(3); - const [firstSpan, secondSpan, thirdSpan] = container.items; - - // [0] invoke_agent - expect(firstSpan!.name).toBe('invoke_agent'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - - // [1] generate_content (doGenerate) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - - // [2] execute_tool (errored) - expect(thirdSpan!.name).toBe('execute_tool getWeather'); - expect(thirdSpan!.status).toBe('error'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + + const generateContentSpan = container.items.find(span => span.name === 'generate_content mock-model-id'); + expect(generateContentSpan).toBeDefined(); + expect(generateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(generateContentSpan!.status).toBe('ok'); + expect(generateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + + const toolSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolSpan).toBeDefined(); + expect(toolSpan!.name).toBe('execute_tool getWeather'); + expect(toolSpan!.status).toBe('error'); + expect(toolSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); }, }) .expect({ @@ -261,18 +332,20 @@ describe('Vercel AI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(7); - const [firstSpan, secondSpan, , , fifthSpan, sixthSpan, seventhSpan] = container.items; - - // invoke_agent spans at [0], [2], [4] - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(fifthSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + const invokeAgentSpans = container.items.filter( + span => span.attributes['sentry.op'].value === 'gen_ai.invoke_agent', + ); + expect(invokeAgentSpans).toHaveLength(3); - // generate_content spans at [1], [3], [5] - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(sixthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + const generateContentSpans = container.items.filter( + span => span.attributes['sentry.op'].value === 'gen_ai.generate_content', + ); + expect(generateContentSpans).toHaveLength(3); - // execute_tool at [6] - expect(seventhSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + const toolSpan = container.items.find( + span => span.attributes['sentry.op'].value === 'gen_ai.execute_tool', + ); + expect(toolSpan).toBeDefined(); }, }) .start() @@ -297,37 +370,41 @@ describe('Vercel AI integration (V6)', () => { .expect({ span: container => { expect(container.items).toHaveLength(4); - const [firstSpan, secondSpan, thirdSpan, fourthSpan] = container.items; - - // [0] invoke_agent (ToolLoopAgent outer span) - expect(firstSpan!.name).toBe('invoke_agent weather_agent'); - expect(firstSpan!.status).toBe('ok'); - expect(firstSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); - expect(firstSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); - - // [1] First doGenerate (returns tool-calls) - expect(secondSpan!.name).toBe('generate_content mock-model-id'); - expect(secondSpan!.status).toBe('ok'); - expect(secondSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(secondSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["tool-calls"]'); - expect(secondSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); - expect(secondSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); - - // [2] Tool execution - expect(thirdSpan!.name).toBe('execute_tool getWeather'); - expect(thirdSpan!.status).toBe('ok'); - expect(thirdSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); - expect(thirdSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); - - // [3] Second doGenerate (returns final text) - expect(fourthSpan!.name).toBe('generate_content mock-model-id'); - expect(fourthSpan!.status).toBe('ok'); - expect(fourthSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); - expect(fourthSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE].value).toBe('["stop"]'); - expect(fourthSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); - expect(fourthSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(25); + const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent weather_agent'); + expect(invokeAgentSpan).toBeDefined(); + expect(invokeAgentSpan!.name).toBe('invoke_agent weather_agent'); + expect(invokeAgentSpan!.status).toBe('ok'); + expect(invokeAgentSpan!.attributes['sentry.op'].value).toBe('gen_ai.invoke_agent'); + expect(invokeAgentSpan!.attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE].value).toBe('mock-model-id'); + + const toolCallsGenerateContentSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]?.value === '["tool-calls"]', + ); + expect(toolCallsGenerateContentSpan).toBeDefined(); + expect(toolCallsGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(toolCallsGenerateContentSpan!.status).toBe('ok'); + expect(toolCallsGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(toolCallsGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(10); + expect(toolCallsGenerateContentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(20); + + const toolSpan = container.items.find(span => span.name === 'execute_tool getWeather'); + expect(toolSpan).toBeDefined(); + expect(toolSpan!.name).toBe('execute_tool getWeather'); + expect(toolSpan!.status).toBe('ok'); + expect(toolSpan!.attributes['sentry.op'].value).toBe('gen_ai.execute_tool'); + expect(toolSpan!.attributes[GEN_AI_TOOL_NAME_ATTRIBUTE].value).toBe('getWeather'); + expect(toolSpan!.attributes[GEN_AI_TOOL_CALL_ID_ATTRIBUTE].value).toBe('call-1'); + expect(toolSpan!.attributes[GEN_AI_TOOL_TYPE_ATTRIBUTE].value).toBe('function'); + + const finalGenerateContentSpan = container.items.find( + span => span.attributes[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]?.value === '["stop"]', + ); + expect(finalGenerateContentSpan).toBeDefined(); + expect(finalGenerateContentSpan!.name).toBe('generate_content mock-model-id'); + expect(finalGenerateContentSpan!.status).toBe('ok'); + expect(finalGenerateContentSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(finalGenerateContentSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE].value).toBe(15); + expect(finalGenerateContentSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE].value).toBe(25); }, }) .start() From f7a842cf16ca42bc0e30422ede377f55f892167c Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Tue, 12 May 2026 09:36:08 +0200 Subject: [PATCH 23/32] ci: Remove codecov reporting (#20803) This does not work really well for us as tests are really fragmented etc, so the coverage is always incomplete. a lot of stuff is covered in e2e tests which are not measured, etc. With this we can safe some (?) processing on CI that we are not really using. --- .github/workflows/build.yml | 37 ------------------------------------- codecov.yml | 15 --------------- 2 files changed, 52 deletions(-) delete mode 100644 codecov.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f19d81a92e3..2d7781c5f43f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -485,13 +485,6 @@ jobs: run: yarn test:ci:browser if: github.event_name != 'pull_request' - - name: Parse and Upload Coverage - if: cancelled() == false - continue-on-error: true - uses: getsentry/codecov-action@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - job_bun_unit_tests: name: Bun Unit Tests needs: [job_get_metadata, job_build] @@ -586,14 +579,6 @@ jobs: env: NODE_VERSION: ${{ matrix.node }} - - name: Parse and Upload Coverage - if: cancelled() == false - continue-on-error: true - uses: getsentry/codecov-action@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - name: ${{ matrix.node }} - job_browser_playwright_tests: name: Playwright ${{ matrix.bundle }}${{ matrix.project && matrix.project != 'chromium' && format(' {0}', @@ -693,18 +678,6 @@ jobs: overwrite: true retention-days: 7 - - name: Parse and Upload Coverage - if: cancelled() == false - continue-on-error: true - uses: getsentry/codecov-action@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - directory: dev-packages/browser-integration-tests - enable-coverage: false - name: - browser-playwright-${{ matrix.bundle }}-${{ matrix.project }}${{ matrix.shard && format('-{0}', - matrix.shard) || '' }} - job_browser_loader_tests: name: PW ${{ matrix.bundle }} Tests needs: [job_get_metadata, job_build, job_build_bundles] @@ -764,16 +737,6 @@ jobs: overwrite: true retention-days: 7 - - name: Parse and Upload Coverage - uses: getsentry/codecov-action@main - if: cancelled() == false - continue-on-error: true - with: - token: ${{ secrets.GITHUB_TOKEN }} - directory: dev-packages/browser-integration-tests - enable-coverage: false - name: browser-loader-${{ matrix.bundle }} - job_check_for_faulty_dts: name: Check for faulty .d.ts files needs: [job_get_metadata, job_build] diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index ae6500091084..000000000000 --- a/codecov.yml +++ /dev/null @@ -1,15 +0,0 @@ -comment: false - -coverage: - status: - project: - default: - enabled: false - patch: - default: - enabled: false - ignore: - - 'packages/deno/**' - -config: - files: changed From 78fe967f36b80e489e2cfdc42e9dd184356f9eec Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Tue, 12 May 2026 09:45:54 +0200 Subject: [PATCH 24/32] ci: Ensure PR reminder workflow considers new sub teams (#20814) I noticed that we forgot to adjust this so it also reminds the new sub teams. --- scripts/pr-review-reminder.mjs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/pr-review-reminder.mjs b/scripts/pr-review-reminder.mjs index aba64c62512e..783508571ade 100644 --- a/scripts/pr-review-reminder.mjs +++ b/scripts/pr-review-reminder.mjs @@ -8,7 +8,7 @@ * @mentions are narrowed as follows: * - Individual users: not [outside collaborators](https://docs.github.com/en/organizations/managing-outside-collaborators) * on this repo (via `repos.listCollaborators` with `affiliation: outside` — repo-scoped, no extra token). - * - Team reviewers: only the org team `team-javascript-sdks` (by slug). + * - Team reviewers: only org teams in `SDK_TEAM_SLUGS` (by slug). * * Business days exclude weekends and a small set of recurring public holidays * (same calendar date each year) for US, CA, and AT. @@ -21,8 +21,13 @@ * await run({ github, context, core }); */ -// Team @mentions only for this slug. Individuals are filtered using outside-collaborator list (see below). -const SDK_TEAM_SLUG = 'team-javascript-sdks'; +// Team @mentions only for these slugs. Individuals are filtered using outside-collaborator list (see below). +const SDK_TEAM_SLUGS = new Set([ + 'team-javascript-sdks', + 'team-javascript-sdks-framework', + 'team-javascript-sdks-browser', + 'team-javascript-sdks-server', +]); // --------------------------------------------------------------------------- // Outside collaborators (repo API — works with default GITHUB_TOKEN). @@ -254,7 +259,7 @@ export default async function run({ github, context, core }) { // Collect overdue team reviewers for (const team of pendingTeams) { - if (team.slug !== SDK_TEAM_SLUG) { + if (!SDK_TEAM_SLUGS.has(team.slug)) { continue; } From 6be4955d7fb4e207f92fb1bd222ab90bd7669a49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 09:47:53 +0200 Subject: [PATCH 25/32] chore(deps): Bump next from 16.2.4 to 16.2.6 in /dev-packages/e2e-tests/test-applications/nextjs-16-streaming (#20811) Bumps [next](https://github.com/vercel/next.js) from 16.2.4 to 16.2.6.
Release notes

Sourced from next's releases.

v16.2.6

This release contains security fixes for the following advisories:

High:

Moderate:

Low:

v16.2.5

This release contains security fixes for the following advisories:

High:

Moderate:

Low:

Commits
  • ee6e79b v16.2.6
  • afa053d Turbopack: Match proxy matchers with webpack implementation (#93594)
  • 97a154e Turbopack: Fix middleware matcher suffix (#93590)
  • 83899bc [backport] Disable build caches for production/staging/force-preview deploys ...
  • 7b222b9 [backport][test] Pin package manager to patch versions (#93595)
  • a8dc24f [backport] Turbopack: more strict vergen setup (#93587)
  • 766148f v16.2.5
  • 0dd9483 fix: add explicit checks for RSC header (#83) (#98)
  • d166096 fix proxy matching for segment prefetch URLs (#89) (#96)
  • 9d50c0b Strip next-resume header from incoming requests (#92)
  • Additional commits viewable in compare view
Maintainer changes

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


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=16.2.4&new-version=16.2.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../test-applications/nextjs-16-streaming/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json index 8e254f4b4657..4fbc3bf64c27 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming/package.json @@ -19,7 +19,7 @@ "@vercel/queue": "^0.1.3", "ai": "^3.0.0", "import-in-the-middle": "^2", - "next": "16.2.4", + "next": "16.2.6", "react": "19.1.0", "react-dom": "19.1.0", "require-in-the-middle": "^8", From ebec4e9eb155cb8a897fea9158c14ffe60bd2412 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 09:48:14 +0200 Subject: [PATCH 26/32] test(deps): Bump hono from 4.12.14 to 4.12.18 in /dev-packages/e2e-tests/test-applications/cloudflare-hono (#20776) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [hono](https://github.com/honojs/hono) from 4.12.14 to 4.12.18.
Release notes

Sourced from hono's releases.

v4.12.18

Security fixes

This release includes fixes for the following security issues:

Cache Middleware ignores Vary: Authorization / Vary: Cookie leading to cross-user cache leakage

Affects: Cache Middleware. Fixes missing cache-skip handling for Vary: Authorization and Vary: Cookie, where a response cached for one authenticated user could be served to other users. GHSA-p77w-8qqv-26rm

CSS Declaration Injection via Style Object Values in JSX SSR

Affects: hono/jsx. Fixes a missing CSS-context escape for style object values and property names, where untrusted input could inject additional CSS declarations. The impact is limited to CSS and does not allow JavaScript execution. GHSA-qp7p-654g-cw7p

Improper validation of NumericDate claims (exp, nbf, iat) in JWT verify()

Affects: hono/utils/jwt. Fixes improper validation of exp, nbf, and iat claims, where falsy, non-finite, or non-numeric values could silently bypass time-based checks instead of being rejected per RFC 7519. GHSA-hm8q-7f3q-5f36


Users who use the JWT helper, hono/jsx, or the Cache middleware are strongly encouraged to upgrade to this version.

v4.12.17

What's Changed

New Contributors

Full Changelog: https://github.com/honojs/hono/compare/v4.12.16...v4.12.17

v4.12.16

Security fixes

This release includes fixes for the following security issues:

Unvalidated JSX Tag Names in hono/jsx May Allow HTML Injection

Affects: hono/jsx. Fixes missing validation of JSX tag names when using jsx() or createElement(), which could allow HTML injection if untrusted input is used as the tag name. GHSA-69xw-7hcm-h432

bodyLimit() can be bypassed for chunked / unknown-length requests

Affects: Body Limit Middleware. Fixes late enforcement for request bodies without a reliable Content-Length (e.g. chunked requests), where oversized requests could reach handlers and return successful responses before being rejected. GHSA-9vqf-7f2p-gf9v

v4.12.15

What's Changed

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.12.14&new-version=4.12.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/cloudflare-hono/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json index 5c4cbe43a5d0..4000aa78e5a1 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", - "hono": "4.12.14" + "hono": "4.12.18" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.31", From e932256dedc947f184914d33f1b74813acac113e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 09:48:55 +0200 Subject: [PATCH 27/32] feat(deps): Bump fast-uri from 3.0.6 to 3.1.2 (#20774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.0.6 to 3.1.2.
Release notes

Sourced from fast-uri's releases.

v3.1.2

⚠️ Security Release

What's Changed

Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.1...v3.1.2

v3.1.1

⚠️ Security Release

What's Changed

New Contributors

Full Changelog: https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.1

v3.1.0

What's Changed

... (truncated)

Commits
  • 919dd8e Bumped v3.1.2
  • c65ba57 fixup: linting
  • 6c86c17 Merge commit from fork
  • a95158a Handle malformed fragment decoding without throwing (#171)
  • cea547c Bumped v3.1.1
  • 876ce79 Merge commit from fork
  • dcdf690 ci: add lock-threads workflow (#169)
  • c860e65 build(deps-dev): bump neostandard from 0.12.2 to 0.13.0 (#167)
  • 9b4c6dc build(deps): bump fastify/workflows/.github/workflows/plugins-ci.yml (#166)
  • 85d09a9 build(deps): bump fastify/workflows/.github/workflows/plugins-ci-package-mana...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fast-uri&package-manager=npm_and_yarn&previous-version=3.0.6&new-version=3.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 78e5b0b5d356..fe328dac5d75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17307,9 +17307,9 @@ fast-text-encoding@^1.0.0: integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec" + integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ== fast-xml-builder@^1.1.5: version "1.1.7" From ca968842fb11d13200a0bbe28937da599e8554d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 10:12:56 +0200 Subject: [PATCH 28/32] chore(deps): Bump @babel/plugin-transform-modules-systemjs from 7.24.1 to 7.29.4 (#20773) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.24.1 to 7.29.4.
Release notes

Sourced from @​babel/plugin-transform-modules-systemjs's releases.

v7.29.4 (2026-05-05)

:bug: Bug Fix

  • babel-plugin-transform-modules-systemjs
    • #17974 [7.x backport]fix(systemjs): improve module string name support (@​JLHwung)

Committers: 1

v7.29.3 (2026-04-30)

:eyeglasses: Spec Compliance

:bug: Bug Fix

  • babel-helper-create-class-features-plugin, babel-plugin-proposal-decorators
    • #17931 fix(decorators): replace super within all removed static elements (@​JLHwung)
  • babel-register
  • babel-compat-data, babel-plugin-bugfix-safari-rest-destructuring-rhs-array, babel-preset-env

:nail_care: Polish

:memo: Documentation

:running_woman: Performance

  • babel-helper-import-to-platform-api, babel-plugin-proposal-import-wasm-source, babel-plugin-transform-json-modules

Committers: 4

v7.29.2 (2026-03-16)

:eyeglasses: Spec Compliance

  • babel-parser

:bug: Bug Fix

  • babel-helpers, babel-plugin-transform-async-generator-functions, babel-preset-env, babel-runtime-corejs3
  • babel-preset-env

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for @​babel/plugin-transform-modules-systemjs since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/plugin-transform-modules-systemjs&package-manager=npm_and_yarn&previous-version=7.24.1&new-version=7.29.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/yarn.lock b/yarn.lock index fe328dac5d75..dca0762a12da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1702,13 +1702,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== -"@babel/helper-hoist-variables@^7.22.5": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" - integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== - dependencies: - "@babel/types" "^7.24.7" - "@babel/helper-member-expression-to-functions@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" @@ -1791,7 +1784,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.25.9", "@babel/helper-validator-identifier@^7.28.5": +"@babel/helper-validator-identifier@^7.25.9", "@babel/helper-validator-identifier@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== @@ -2356,14 +2349,14 @@ "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-modules-systemjs@^7.18.9", "@babel/plugin-transform-modules-systemjs@^7.24.1": - version "7.24.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz#2b9625a3d4e445babac9788daec39094e6b11e3e" - integrity sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA== + version "7.29.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20" + integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w== dependencies: - "@babel/helper-hoist-variables" "^7.22.5" - "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.24.0" - "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.29.0" "@babel/plugin-transform-modules-umd@^7.18.6", "@babel/plugin-transform-modules-umd@^7.24.1": version "7.24.1" From 0411061ab2d15653d3df39c156e544783cbcd70d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 08:32:06 +0000 Subject: [PATCH 29/32] chore(deps): Bump next from 15.5.15 to 15.5.18 in /dev-packages/e2e-tests/test-applications/nextjs-15 (#20818) Bumps [next](https://github.com/vercel/next.js) from 15.5.15 to 15.5.18.
Release notes

Sourced from next's releases.

v15.5.18

This release contains security fixes for the following advisories:

High:

Moderate:

Low:

v15.5.16

This release contains security fixes for the following advisories:

High:

Moderate:

Low:

Commits
  • 9ff92ce v15.5.18
  • 00ebe23 [backport] Disable build caches for production/staging/force-preview deploys ...
  • 62c97ab v15.5.17
  • 423623a Turbopack: Match proxy matchers with webpack implementation (#93594)
  • fa78739 Turbopack: Fix middleware matcher suffix (#93590)
  • 36e62c6 [backport] Turbopack: more strict vergen setup (#93588)
  • 36589b5 [backport][test] Pin package manager to patch versions (#93596)
  • ad6fd4e v15.5.16
  • 79d7dff Ignore malformed CSP nonce headers (#103)
  • c4f6908 router-server: guard upgrade proxy against absolute-url SSRF (#77) (#102)
  • Additional commits viewable in compare view
Maintainer changes

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


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=next&package-manager=npm_and_yarn&previous-version=15.5.15&new-version=15.5.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-packages/e2e-tests/test-applications/nextjs-15/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 acbe56d0b5f1..25fc16f8702e 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.15", + "next": "15.5.18", "react": "latest", "react-dom": "latest", "typescript": "~5.0.0", From 9f99463482a06e6b9394c929144d943673c83107 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 12 May 2026 11:50:37 +0200 Subject: [PATCH 30/32] feat(browser): Migrate browser profiling thread data to span attributes (#20800) Migrate `thread.id/thread.name` assignment from `processEvent` hook to `span.setAttribute()` at span start, covering legacy, trace, and manual profiling modes. closes https://github.com/getsentry/sentry-javascript/issues/20379 --- .../subject.js | 37 ++++ .../traceLifecycleMode_streamed-spans/test.ts | 36 ++++ packages/browser/src/profiling/UIProfiler.ts | 18 +- packages/browser/src/profiling/integration.ts | 15 +- .../src/profiling/startProfileForSpan.ts | 12 +- packages/browser/src/profiling/utils.ts | 38 +--- .../browser/test/profiling/UIProfiler.test.ts | 173 ++++++++++++++++++ .../test/profiling/integration.test.ts | 60 ++++++ 8 files changed, 347 insertions(+), 42 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/test.ts diff --git a/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/subject.js new file mode 100644 index 000000000000..8ad2a8eac5af --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/subject.js @@ -0,0 +1,37 @@ +import * as Sentry from '@sentry/browser'; +import { browserProfilingIntegration, spanStreamingIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [spanStreamingIntegration(), browserProfilingIntegration()], + tracesSampleRate: 1, + traceLifecycle: 'stream', + profileSessionSampleRate: 1, + profileLifecycle: 'trace', +}); + +function fibonacci(n) { + if (n <= 1) { + return n; + } + return fibonacci(n - 1) + fibonacci(n - 2); +} + +await Sentry.startSpanManual({ name: 'root-fibonacci', parentSpan: null }, async span => { + fibonacci(40); + + Sentry.startSpan({ name: 'child-span-1', parentSpan: span }, () => { + fibonacci(20); + }); + + Sentry.startSpan({ name: 'child-span-2', parentSpan: span }, () => { + fibonacci(20); + }); + + await new Promise(resolve => setTimeout(resolve, 40)); + span.end(); +}); + +await client?.flush(5000); diff --git a/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/test.ts new file mode 100644 index 000000000000..f033302c9579 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_streamed-spans/test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { waitForStreamedSpans } from '../../../utils/spanUtils'; + +sentryTest( + 'attaches thread.id and thread.name to streamed spans (trace mode)', + async ({ page, getLocalTestUrl, browserName }) => { + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + const spansPromise = waitForStreamedSpans(page, receivedSpans => { + return receivedSpans.some(s => s.name === 'root-fibonacci'); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } }); + await page.goto(url); + + const spans = await spansPromise; + + const rootSpan = spans.find(s => s.name === 'root-fibonacci'); + expect(rootSpan).toBeDefined(); + + expect(rootSpan!.attributes?.['thread.id']).toEqual({ type: 'string', value: '0' }); + expect(rootSpan!.attributes?.['thread.name']).toEqual({ type: 'string', value: 'main' }); + + const childSpans = spans.filter(s => s.name === 'child-span-1' || s.name === 'child-span-2'); + expect(childSpans.length).toBeGreaterThanOrEqual(1); + + for (const child of childSpans) { + expect(child.attributes?.['thread.id']).toEqual({ type: 'string', value: '0' }); + expect(child.attributes?.['thread.name']).toEqual({ type: 'string', value: 'main' }); + } + }, +); diff --git a/packages/browser/src/profiling/UIProfiler.ts b/packages/browser/src/profiling/UIProfiler.ts index aa9c6a5f5fdf..e64f7720710b 100644 --- a/packages/browser/src/profiling/UIProfiler.ts +++ b/packages/browser/src/profiling/UIProfiler.ts @@ -11,7 +11,13 @@ import { import type { BrowserOptions } from '../client'; import { DEBUG_BUILD } from './../debug-build'; import type { JSSelfProfiler } from './jsSelfProfiling'; -import { createProfileChunkPayload, shouldProfileSession, startJSSelfProfile, validateProfileChunk } from './utils'; +import { + createProfileChunkPayload, + setThreadAttributes, + shouldProfileSession, + startJSSelfProfile, + validateProfileChunk, +} from './utils'; const CHUNK_INTERVAL_MS = 60_000; // 1 minute // Maximum length for trace lifecycle profiling per root span (e.g. if spanEnd never fires) @@ -78,6 +84,12 @@ export class UIProfiler implements ContinuousProfiler { if (lifecycleMode === 'trace') { this._setupTraceLifecycleListeners(client); } + + client.on('spanStart', span => { + if (this._isRunning) { + setThreadAttributes(span); + } + }); } /** Starts UI profiling (only effective in 'manual' mode and when sampled). */ @@ -142,6 +154,10 @@ export class UIProfiler implements ContinuousProfiler { this._beginProfiling(); } + + if (this._isRunning) { + setThreadAttributes(rootSpan); + } } /** diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 22706117fa74..ed438d15a7b3 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -8,12 +8,13 @@ import { UIProfiler } from './UIProfiler'; import type { ProfiledEvent } from './utils'; import { addProfilesToEnvelope, - attachProfiledThreadToEvent, createProfilingEvent, findProfiledTransactionsFromEnvelope, getActiveProfilesCount, hasLegacyProfiling, isAutomatedPageLoadSpan, + PROFILED_ROOT_SPANS, + setThreadAttributes, shouldProfileSpanLegacy, takeProfileFromGlobalCache, } from './utils'; @@ -92,8 +93,13 @@ const _browserProfilingIntegration = (() => { } client.on('spanStart', (span: Span) => { - if (span === getRootSpan(span) && shouldProfileSpanLegacy(span)) { - startProfileForSpan(span); + const rootSpan = getRootSpan(span); + if (span === rootSpan) { + if (shouldProfileSpanLegacy(span)) { + startProfileForSpan(span); + } + } else if (PROFILED_ROOT_SPANS.has(rootSpan)) { + setThreadAttributes(span); } }); @@ -151,9 +157,6 @@ const _browserProfilingIntegration = (() => { }); } }, - processEvent(event) { - return attachProfiledThreadToEvent(event); - }, }; }) satisfies IntegrationFn; diff --git a/packages/browser/src/profiling/startProfileForSpan.ts b/packages/browser/src/profiling/startProfileForSpan.ts index 6c36c6bf84aa..974cf9e8e9b2 100644 --- a/packages/browser/src/profiling/startProfileForSpan.ts +++ b/packages/browser/src/profiling/startProfileForSpan.ts @@ -3,7 +3,14 @@ import { debug, getCurrentScope, spanToJSON, timestampInSeconds, uuid4 } from '@ import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import type { JSSelfProfile } from './jsSelfProfiling'; -import { addProfileToGlobalCache, isAutomatedPageLoadSpan, MAX_PROFILE_DURATION_MS, startJSSelfProfile } from './utils'; +import { + addProfileToGlobalCache, + isAutomatedPageLoadSpan, + MAX_PROFILE_DURATION_MS, + PROFILED_ROOT_SPANS, + setThreadAttributes, + startJSSelfProfile, +} from './utils'; /** * Wraps startTransaction and stopTransaction with profiling related logic. @@ -48,6 +55,9 @@ export function startProfileForSpan(span: Span): void { start_timestamp: startTimestamp, }); + PROFILED_ROOT_SPANS.add(span); + setThreadAttributes(span); + /** * Idempotent handler for profile stop */ diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 12446e6fb986..0d7d413428dd 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -786,39 +786,9 @@ export function addProfileToGlobalCache(profile_id: string, profile: JSSelfProfi } } -/** - * Attaches the profiled thread information to the event's trace context. - */ -export function attachProfiledThreadToEvent(event: Event): Event { - if (!event?.contexts?.profile) { - return event; - } - - if (!event.contexts) { - return event; - } - - // Only mutate the trace context when it already has a trace_id — that - // guarantees `applySpanToEvent` has already run, and we are not creating a partial trace context from scratch. - if (event.contexts.trace?.trace_id) { - event.contexts.trace = { - ...event.contexts.trace, - data: { - ...(event.contexts.trace.data ?? {}), - ['thread.id']: PROFILER_THREAD_ID_STRING, - ['thread.name']: PROFILER_THREAD_NAME, - }, - }; - } - - // Attach thread info to individual spans so that spans can be associated with the profiled thread on the UI even if contexts are missing. - event.spans?.forEach(span => { - span.data = { - ...(span.data || {}), - ['thread.id']: PROFILER_THREAD_ID_STRING, - ['thread.name']: PROFILER_THREAD_NAME, - }; - }); +export const PROFILED_ROOT_SPANS = new WeakSet(); - return event; +export function setThreadAttributes(span: Span): void { + span.setAttribute('thread.id', PROFILER_THREAD_ID_STRING); + span.setAttribute('thread.name', PROFILER_THREAD_NAME); } diff --git a/packages/browser/test/profiling/UIProfiler.test.ts b/packages/browser/test/profiling/UIProfiler.test.ts index d1702cfcbd49..19c72dffcacd 100644 --- a/packages/browser/test/profiling/UIProfiler.test.ts +++ b/packages/browser/test/profiling/UIProfiler.test.ts @@ -583,6 +583,100 @@ describe('Browser Profiling v2 trace lifecycle', () => { }); }); + describe('thread attributes', () => { + it('sets thread.id and thread.name on root span', async () => { + vi.useRealTimers(); + mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForTraceLifecycle(send), + }); + + Sentry.startSpan({ name: 'root-thread-attrs', parentSpan: null, forceTransaction: true }, () => { + /* empty */ + }); + + const client = Sentry.getClient(); + await client?.flush(1000); + + const calls = send.mock.calls; + const txnCall = calls.find(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction'); + const transaction = txnCall?.[0]?.[1]?.[0]?.[1]; + + expect(transaction.transaction).toBe('root-thread-attrs'); + expect(transaction.contexts.trace.data['thread.id']).toBe('0'); + expect(transaction.contexts.trace.data['thread.name']).toBe('main'); + }); + + it('sets thread.id and thread.name on child spans', async () => { + vi.useRealTimers(); + mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForTraceLifecycle(send), + }); + + Sentry.startSpan({ name: 'root-with-children', parentSpan: null, forceTransaction: true }, () => { + Sentry.startSpan({ name: 'child-span-1' }, () => { + /* empty */ + }); + Sentry.startSpan({ name: 'child-span-2' }, () => { + /* empty */ + }); + }); + + const client = Sentry.getClient(); + await client?.flush(1000); + + const calls = send.mock.calls; + const txnCall = calls.find(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction'); + const transaction = txnCall?.[0]?.[1]?.[0]?.[1]; + + expect(transaction.transaction).toBe('root-with-children'); + expect(transaction.spans).toHaveLength(2); + for (const span of transaction.spans) { + expect(span.data['thread.id']).toBe('0'); + expect(span.data['thread.name']).toBe('main'); + } + }); + + it('does not set thread attributes when session is not sampled', async () => { + vi.useRealTimers(); + mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForTraceLifecycle(send), + profileSessionSampleRate: 0, + }); + + Sentry.startSpan({ name: 'unsampled-root', parentSpan: null, forceTransaction: true }, () => { + Sentry.startSpan({ name: 'unsampled-child' }, () => { + /* empty */ + }); + }); + + const client = Sentry.getClient(); + await client?.flush(1000); + + const calls = send.mock.calls; + const txnCall = calls.find(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction'); + expect(txnCall).toBeDefined(); + + const transaction = txnCall?.[0]?.[1]?.[0]?.[1]; + expect(transaction.transaction).toBe('unsampled-root'); + expect(transaction.contexts.trace.data['thread.id']).toBeUndefined(); + expect(transaction.contexts.trace.data['thread.name']).toBeUndefined(); + + expect(transaction.spans).toHaveLength(1); + expect(transaction.spans[0].description).toBe('unsampled-child'); + expect(transaction.spans[0].data['thread.id']).toBeUndefined(); + expect(transaction.spans[0].data['thread.name']).toBeUndefined(); + }); + }); + it('calling start and stop in trace lifecycle prints warnings', async () => { const { stop } = mockProfiler(); const send = vi.fn().mockResolvedValue(undefined); @@ -848,4 +942,83 @@ describe('Browser Profiling v2 manual lifecycle', () => { expect(firstProfilerId).toBe(thirdProfilerId); // same profiler_id across session }); }); + + describe('thread attributes', () => { + it('sets thread.id and thread.name on spans created while profiling is active', async () => { + vi.useRealTimers(); + mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + }); + + Sentry.uiProfiler.startProfiler(); + + Sentry.startSpan({ name: 'manual-profiled-root', parentSpan: null, forceTransaction: true }, () => { + Sentry.startSpan({ name: 'manual-profiled-child' }, () => { + /* empty */ + }); + }); + + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + const client = Sentry.getClient(); + await client?.flush(1000); + + const calls = send.mock.calls; + const txnCall = calls.find(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction'); + const transaction = txnCall?.[0]?.[1]?.[0]?.[1]; + + expect(transaction.transaction).toBe('manual-profiled-root'); + expect(transaction.contexts.trace.data['thread.id']).toBe('0'); + expect(transaction.contexts.trace.data['thread.name']).toBe('main'); + + expect(transaction.spans).toHaveLength(1); + expect(transaction.spans[0].description).toBe('manual-profiled-child'); + expect(transaction.spans[0].data['thread.id']).toBe('0'); + expect(transaction.spans[0].data['thread.name']).toBe('main'); + }); + + it('does not set thread attributes on spans created outside of profiling window', async () => { + vi.useRealTimers(); + mockProfiler(); + const send = vi.fn().mockResolvedValue(undefined); + + Sentry.init({ + ...getBaseOptionsForManualLifecycle(send), + }); + + // Create span BEFORE profiling starts + Sentry.startSpan({ name: 'before-profiling', parentSpan: null, forceTransaction: true }, () => { + /* empty */ + }); + + Sentry.uiProfiler.startProfiler(); + Sentry.uiProfiler.stopProfiler(); + await Promise.resolve(); + + // Create span AFTER profiling stops + Sentry.startSpan({ name: 'after-profiling', parentSpan: null, forceTransaction: true }, () => { + /* empty */ + }); + + const client = Sentry.getClient(); + await client?.flush(1000); + + const calls = send.mock.calls; + const txnCalls = calls.filter(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction'); + const transactions = txnCalls.map(call => call?.[0]?.[1]?.[0]?.[1]); + + expect(transactions).toHaveLength(2); + expect(transactions[0].transaction).toBe('before-profiling'); + expect(transactions[1].transaction).toBe('after-profiling'); + + for (const transaction of transactions) { + expect(transaction.contexts.trace.data['thread.id']).toBeUndefined(); + expect(transaction.contexts.trace.data['thread.name']).toBeUndefined(); + } + }); + }); }); diff --git a/packages/browser/test/profiling/integration.test.ts b/packages/browser/test/profiling/integration.test.ts index 9ec370bc827f..b281c54c578c 100644 --- a/packages/browser/test/profiling/integration.test.ts +++ b/packages/browser/test/profiling/integration.test.ts @@ -108,4 +108,64 @@ describe('BrowserProfilingIntegration', () => { const lifecycle = client?.getOptions()?.profileLifecycle; expect(lifecycle).toBe('manual'); }); + + describe('legacy profiling thread attributes', () => { + it('sets thread.id and thread.name on root span and child spans', async () => { + class MockProfiler { + stopped: boolean = false; + constructor(_opts: { sampleInterval: number; maxBufferSize: number }) {} + stop() { + this.stopped = true; + return Promise.resolve({ + frames: [{ name: 'test_fn', line: 1, column: 1 }], + stacks: [{ frameId: 0, parentId: undefined }], + samples: [ + { stackId: 0, timestamp: 0 }, + { stackId: 0, timestamp: 100 }, + ], + resources: [], + } as JSSelfProfile); + } + } + + // @ts-expect-error this is a mock constructor + window.Profiler = MockProfiler; + + const send = vi.fn().mockResolvedValue(undefined); + const client = Sentry.init({ + tracesSampleRate: 1, + profilesSampleRate: 1, + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + transport: _opts => ({ + flush: vi.fn().mockResolvedValue(true), + send, + }), + integrations: [Sentry.browserProfilingIntegration()], + }); + + Sentry.startSpan({ name: 'legacy-root', parentSpan: null, forceTransaction: true }, () => { + Sentry.startSpan({ name: 'legacy-child' }, () => { + /* empty */ + }); + }); + + await client!.flush(1000); + + const txnCall = send.mock.calls.find(call => call?.[0]?.[1]?.[0]?.[0]?.type === 'transaction'); + expect(txnCall).toBeDefined(); + + const transaction = txnCall?.[0]?.[1]?.[0]?.[1]; + + // Root span thread attributes are in contexts.trace.data + expect(transaction.contexts.trace.data['thread.id']).toBe('0'); + expect(transaction.contexts.trace.data['thread.name']).toBe('main'); + + // Child span thread attributes + expect(transaction.spans).toHaveLength(1); + expect(transaction.spans[0].data['thread.id']).toBe('0'); + expect(transaction.spans[0].data['thread.name']).toBe('main'); + + (window as any).Profiler = undefined; + }); + }); }); From a93d32f553539734962da1a5c63e2e8c07a70e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peer=20St=C3=B6cklmair?= Date: Tue, 12 May 2026 11:54:51 +0200 Subject: [PATCH 31/32] feat(deps): Bump bundler plugins to 5.3.0 (#20820) This bumps all bundler plugins to 5.3.0 --- .../browser-webworker-vite/package.json | 2 +- .../debug-id-sourcemaps/package.json | 2 +- .../hydrogen-react-router-7/package.json | 2 +- .../remix-hydrogen/package.json | 2 +- packages/astro/package.json | 2 +- packages/gatsby/package.json | 2 +- packages/nextjs/package.json | 4 +- packages/nitro/package.json | 2 +- packages/nuxt/package.json | 4 +- packages/react-router/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/tanstackstart-react/package.json | 2 +- yarn.lock | 51 +++++++++---------- 14 files changed, 40 insertions(+), 41 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json index 71cae14a0120..22489d121695 100644 --- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz", - "@sentry/vite-plugin": "^5.2.0" + "@sentry/vite-plugin": "^5.3.0" }, "volta": { "node": "20.19.2", diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index d7599f0e332d..29e816de1116 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -15,7 +15,7 @@ "devDependencies": { "rollup": "^4.35.0", "vitest": "^0.34.6", - "@sentry/rollup-plugin": "^5.2.0" + "@sentry/rollup-plugin": "^5.3.0" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json index 56789ec7cedb..d8fed819c425 100644 --- a/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json +++ b/dev-packages/e2e-tests/test-applications/hydrogen-react-router-7/package.json @@ -16,7 +16,7 @@ "dependencies": { "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "@sentry/vite-plugin": "^5.2.0", + "@sentry/vite-plugin": "^5.3.0", "@shopify/hydrogen": "2025.5.0", "@shopify/remix-oxygen": "^3.0.0", "graphql": "^16.10.0", diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json index b51c2868f415..04b32ff7d222 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -19,7 +19,7 @@ "@remix-run/cloudflare-pages": "^2.17.4", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/remix": "file:../../packed/sentry-remix-packed.tgz", - "@sentry/vite-plugin": "^5.2.0", + "@sentry/vite-plugin": "^5.3.0", "@shopify/hydrogen": "2025.4.0", "@shopify/remix-oxygen": "2.0.10", "graphql": "^16.6.0", diff --git a/packages/astro/package.json b/packages/astro/package.json index 0a1ccf12c590..5daba98966f1 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -59,7 +59,7 @@ "@sentry/browser": "10.52.0", "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", - "@sentry/vite-plugin": "^5.2.0" + "@sentry/vite-plugin": "^5.3.0" }, "devDependencies": { "astro": "^3.5.0", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index d457f506e4dc..bf75a7de04fb 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "10.52.0", "@sentry/react": "10.52.0", - "@sentry/webpack-plugin": "^5.2.0" + "@sentry/webpack-plugin": "^5.3.0" }, "peerDependencies": { "gatsby": "^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index f8c1126d0041..a389bd02df29 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -80,13 +80,13 @@ "@opentelemetry/semantic-conventions": "^1.40.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "10.52.0", - "@sentry/bundler-plugin-core": "^5.2.0", + "@sentry/bundler-plugin-core": "^5.3.0", "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/opentelemetry": "10.52.0", "@sentry/react": "10.52.0", "@sentry/vercel-edge": "10.52.0", - "@sentry/webpack-plugin": "^5.2.0", + "@sentry/webpack-plugin": "^5.3.0", "rollup": "^4.60.3", "stacktrace-parser": "^0.1.11" }, diff --git a/packages/nitro/package.json b/packages/nitro/package.json index eccc8e5ded85..be7fbecb2f98 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -35,7 +35,7 @@ "nitro": ">=3.0.0-0 <4.0.0 || 3.0.260311-beta || 3.0.260415-beta" }, "dependencies": { - "@sentry/bundler-plugin-core": "^5.2.0", + "@sentry/bundler-plugin-core": "^5.3.0", "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/opentelemetry": "10.52.0" diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 9f3e89924965..14bbdc25c0ae 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -60,8 +60,8 @@ "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/node-core": "10.52.0", - "@sentry/rollup-plugin": "^5.2.0", - "@sentry/vite-plugin": "^5.2.0", + "@sentry/rollup-plugin": "^5.3.0", + "@sentry/vite-plugin": "^5.3.0", "@sentry/vue": "10.52.0", "local-pkg": "^1.1.2" }, diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 19b76c1c8c73..da668dfd3a37 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -54,7 +54,7 @@ "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/react": "10.52.0", - "@sentry/vite-plugin": "^5.2.0", + "@sentry/vite-plugin": "^5.3.0", "glob": "^13.0.6" }, "devDependencies": { diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index f7be14121d07..07cab4191866 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -69,7 +69,7 @@ "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/solid": "10.52.0", - "@sentry/vite-plugin": "^5.2.0" + "@sentry/vite-plugin": "^5.3.0" }, "devDependencies": { "@solidjs/router": "^0.15.0", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 829737f6ad40..e63e43608911 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -51,7 +51,7 @@ "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/svelte": "10.52.0", - "@sentry/vite-plugin": "^5.2.0", + "@sentry/vite-plugin": "^5.3.0", "@sveltejs/acorn-typescript": "^1.0.9", "acorn": "^8.14.0", "magic-string": "~0.30.0", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index b53254b5d728..a6dd0076d920 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -69,7 +69,7 @@ "@sentry/core": "10.52.0", "@sentry/node": "10.52.0", "@sentry/react": "10.52.0", - "@sentry/vite-plugin": "^5.2.0" + "@sentry/vite-plugin": "^5.3.0" }, "devDependencies": { "vite": "^5.4.11" diff --git a/yarn.lock b/yarn.lock index dca0762a12da..06a319881032 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7729,18 +7729,18 @@ fflate "^0.4.4" mitt "^3.0.0" -"@sentry/babel-plugin-component-annotate@5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.2.0.tgz#6d6f3c47d7f795f5dfbb9b59abef6ab33e5e7f2d" - integrity sha512-8LbOI5Kzb5F0+7LVQPi2+zGz1iPiRRFhM+7uZ/ZQ33L9BmDOYNIy3xWxCfMw2JCuMXXaxF47XCjGmR22/B0WPg== +"@sentry/babel-plugin-component-annotate@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.3.0.tgz#356218f747969f9af970987dcf0f17ec81d6e50c" + integrity sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA== -"@sentry/bundler-plugin-core@5.2.0", "@sentry/bundler-plugin-core@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.2.0.tgz#805ab7820b23d21ba5267e97db7300df35aede88" - integrity sha512-+C0x4gEIJRgoMwyRFGx+TFiJ1Po2BZlT1v61+PnouiaprKL5qtZG8n5PXx/5LPLDsVjSIcXjnDrTz9aSm8SJ3w== +"@sentry/bundler-plugin-core@5.3.0", "@sentry/bundler-plugin-core@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.3.0.tgz#2772866dcb076c36721d2acab1010a6fc0b3ff2f" + integrity sha512-L5T60sWdAI3qWwdg3Ptwek/0TY59PERrxyqp4XMUkroayQvGd9r5dIW9Q1kSeXX9iJ442nXbFZKAOyCKV4Z13Q== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "5.2.0" + "@sentry/babel-plugin-component-annotate" "5.3.0" "@sentry/cli" "^2.58.5" dotenv "^16.3.1" find-up "^5.0.0" @@ -7807,28 +7807,28 @@ "@sentry/cli-win32-i686" "2.58.5" "@sentry/cli-win32-x64" "2.58.5" -"@sentry/rollup-plugin@5.2.0", "@sentry/rollup-plugin@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-5.2.0.tgz#41601fa35fdcf9a43cff9807cdca012780d2fd5b" - integrity sha512-a8LfpvcYMFtFSroro5MpCcOoS528LeLfUHzxWURnpofOnY+Aso9Si4y4dFlna+RKqxCXjmFbn6CLnfI+YrHysQ== +"@sentry/rollup-plugin@5.3.0", "@sentry/rollup-plugin@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-5.3.0.tgz#1dbfbee8d5d2a0f6acc245f4dd05e46c66291710" + integrity sha512-hgPGPYdQJ/G1cGYOxAb7d4z3V+/k/E5/P/5TFPEEBLuIbFFk+JG0CISUDJdzXJjO382Lb99PBJuXGbueBmO79w== dependencies: - "@sentry/bundler-plugin-core" "5.2.0" + "@sentry/bundler-plugin-core" "5.3.0" magic-string "~0.30.8" -"@sentry/vite-plugin@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-5.2.0.tgz#eca4c5eebe00696ded98e055f185faf846886f19" - integrity sha512-4Jo3ixBspso5HY81PDvZdRXkH9wYGVmcw/0a2IX9ejbyKBdHqkYg4IhAtNqGUAyGuHwwRS9Y1S+sCMvrXv6htw== +"@sentry/vite-plugin@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-5.3.0.tgz#6cb63bfc1b0d1613a151de456b9c66f6cb21b1d2" + integrity sha512-qcoSzo4n2MulVQ70UUPLq6dTleb2a2HwL2wuwvAgWhPChrYTuk6A6mDg6aQb9fairPAwFPiU9PzOANpoDJcz1A== dependencies: - "@sentry/bundler-plugin-core" "5.2.0" - "@sentry/rollup-plugin" "5.2.0" + "@sentry/bundler-plugin-core" "5.3.0" + "@sentry/rollup-plugin" "5.3.0" -"@sentry/webpack-plugin@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-5.2.0.tgz#6d190986198545c4b3046205f99ffcc601e8d936" - integrity sha512-ssV/uJK3ixf8UHBrNdLBXcnprUwppJNilbFv+19I81KTH4gVwzKXsVTMO91j6lyAXtk2mORwmEFwxZqScFfc7g== +"@sentry/webpack-plugin@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-5.3.0.tgz#7c28fbc3f4fbfe07c51f63f4a403c9c7a28ca9d7" + integrity sha512-i3OQUrS0FZlXLgq57RIKDp+vHHzuvYKPCKewAPXULWKMsBXFGhP6veGRQ+6To/pmZkkXjEX5ofVNDy9C3jEPKQ== dependencies: - "@sentry/bundler-plugin-core" "5.2.0" + "@sentry/bundler-plugin-core" "5.3.0" "@shikijs/core@1.29.2": version "1.29.2" @@ -28486,7 +28486,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" From e01d66cd2c755796477ab03f0092a0392d77b43d Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 12 May 2026 12:18:02 +0200 Subject: [PATCH 32/32] meta(changelog): Update changelog for 10.53.0 Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b177648c7a3d..0e1bac4c474d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @dmmulroy and @SAY-5. Thank you for your contributions! +## 10.53.0 ### Important Changes @@ -23,6 +23,44 @@ Work in this release was contributed by @dmmulroy and @SAY-5. Thank you for your }); ``` +### Other Changes + +- feat(browser): Migrate browser profiling thread data to span attributes ([#20800](https://github.com/getsentry/sentry-javascript/pull/20800)) +- feat(core): Add `addConsoleInstrumentationFilter` utility ([#20790](https://github.com/getsentry/sentry-javascript/pull/20790)) +- feat(core): Add `applicationKey` to `BuildTimeOptionsBase` ([#20789](https://github.com/getsentry/sentry-javascript/pull/20789)) +- feat(core): split exports by browser/server for bundle size ([#20435](https://github.com/getsentry/sentry-javascript/pull/20435)) +- feat(nextjs): Add top-level `applicationKey` option ([#20794](https://github.com/getsentry/sentry-javascript/pull/20794)) +- feat(node): Support Node 26 ([#20710](https://github.com/getsentry/sentry-javascript/pull/20710)) +- feat(profiling-node): Bump `@sentry-internal/node-cpu-profiler` to 2.4.0 ([#20720](https://github.com/getsentry/sentry-javascript/pull/20720)) +- fix(cloudflare): avoid flush lock self-wait ([#20719](https://github.com/getsentry/sentry-javascript/pull/20719)) +- fix(hono): Capture transaction name on request for correct culprit ([#20801](https://github.com/getsentry/sentry-javascript/pull/20801)) +- fix(mcp): retroactively wrap handlers registered before wrapMcpServerWithSentry ([#20699](https://github.com/getsentry/sentry-javascript/pull/20699)) +- fix(node-core): Guard against undefined util.getSystemErrorMap ([#20660](https://github.com/getsentry/sentry-javascript/pull/20660)) +- fix(replay): Capture aborted/errored fetch requests in replay network tab ([#20722](https://github.com/getsentry/sentry-javascript/pull/20722)) + +
+ Internal Changes + +- chore: bump replay dependencies ([#20746](https://github.com/getsentry/sentry-javascript/pull/20746)) +- chore: Typo intergation -> integration ([#20799](https://github.com/getsentry/sentry-javascript/pull/20799)) +- chore(deps): Bump @babel/plugin-transform-modules-systemjs from 7.24.1 to 7.29.4 ([#20773](https://github.com/getsentry/sentry-javascript/pull/20773)) +- chore(deps): Bump next from 15.5.15 to 15.5.18 in /dev-packages/e2e-tests/test-applications/nextjs-15 ([#20818](https://github.com/getsentry/sentry-javascript/pull/20818)) +- chore(deps): Bump next from 16.2.4 to 16.2.6 in /dev-packages/e2e-tests/test-applications/nextjs-16-streaming ([#20811](https://github.com/getsentry/sentry-javascript/pull/20811)) +- chore(deps): Bump rollup from 4.59.0 to 4.60.3 ([#20716](https://github.com/getsentry/sentry-javascript/pull/20716)) +- ci: Ensure PR reminder workflow considers new sub teams ([#20814](https://github.com/getsentry/sentry-javascript/pull/20814)) +- ci: Remove codecov reporting ([#20803](https://github.com/getsentry/sentry-javascript/pull/20803)) +- feat(deps): Bump bundler plugins to 5.3.0 ([#20820](https://github.com/getsentry/sentry-javascript/pull/20820)) +- feat(deps): Bump fast-uri from 3.0.6 to 3.1.2 ([#20774](https://github.com/getsentry/sentry-javascript/pull/20774)) +- feat(deps): Bump hono from 4.12.16 to 4.12.18 ([#20777](https://github.com/getsentry/sentry-javascript/pull/20777)) +- test(cloudflare-hono): fix 'occured' -> 'occurred' typo in error log ([#20783](https://github.com/getsentry/sentry-javascript/pull/20783)) +- test(deps): Bump hono from 4.12.14 to 4.12.16 ([#20712](https://github.com/getsentry/sentry-javascript/pull/20712)) +- test(deps): Bump hono from 4.12.14 to 4.12.18 in /dev-packages/e2e-tests/test-applications/cloudflare-hono ([#20776](https://github.com/getsentry/sentry-javascript/pull/20776)) +- test(e2e): Pin astro version in astro-6 test app ([#20709](https://github.com/getsentry/sentry-javascript/pull/20709)) + +
+ +Work in this release was contributed by @dmmulroy and @SAY-5. Thank you for your contributions! + ## 10.52.0 ### Important Changes