diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md
index a512d79fa435..d70f36ff6c94 100644
--- a/.cursor/BUGBOT.md
+++ b/.cursor/BUGBOT.md
@@ -40,3 +40,4 @@ Do not flag the issues below if they appear in tests.
- If there's no direct span that's wrapping the captured exception, apply a proper `type` value, following the same naming
convention as the `SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN` value.
- When calling `startSpan`, check if error cases are handled. If flag that it might make sense to try/catch and call `captureException`.
+- When calling `generateInstrumentationOnce`, the passed in name MUST match the name of the integration that uses it. If there are more than one instrumentations, they need to follow the pattern `${INSTRUMENTATION_NAME}.some-suffix`.
diff --git a/.size-limit.js b/.size-limit.js
index dd65a987d506..490195900900 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -233,7 +233,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '148 KB',
+ limit: '149 KB',
},
{
name: '@sentry/node - without tracing',
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12cdde3b7bea..6529fcfe59cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,45 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 10.6.0
+
+### Important Changes
+
+- **feat(node): Add Anthropic AI integration ([#17348](https://github.com/getsentry/sentry-javascript/pull/17348))**
+
+This release adds support for automatically tracing Anthropic AI SDK requests, providing better observability for AI-powered applications.
+
+- **fix(core): Instrument invoke_agent root span, and support Vercel `ai` v5 ([#17395](https://github.com/getsentry/sentry-javascript/pull/17395))**
+
+This release makes the Sentry `vercelAiIntegration` compatible with version 5 of Vercel `ai`.
+
+- **docs(nuxt): Remove beta notice ([#17400](https://github.com/getsentry/sentry-javascript/pull/17400))**
+
+The Sentry Nuxt SDK is now considered stable and no longer in beta!
+
+### Other Changes
+
+- feat(astro): Align options with shared build time options type ([#17396](https://github.com/getsentry/sentry-javascript/pull/17396))
+- feat(aws): Add support for automatic wrapping in ESM ([#17407](https://github.com/getsentry/sentry-javascript/pull/17407))
+- feat(node): Add an instrumentation interface for Hono ([#17366](https://github.com/getsentry/sentry-javascript/pull/17366))
+- fix(browser): Use `DedicatedWorkerGlobalScope` global object type in `registerWebWorker` ([#17447](https://github.com/getsentry/sentry-javascript/pull/17447))
+- fix(core): Only consider ingest endpoint requests when checking `isSentryRequestUrl` ([#17393](https://github.com/getsentry/sentry-javascript/pull/17393))
+- fix(node): Fix preloading of instrumentation ([#17403](https://github.com/getsentry/sentry-javascript/pull/17403))
+
+
+ Internal Changes
+
+- chore: Add external contributor to CHANGELOG.md ([#17449](https://github.com/getsentry/sentry-javascript/pull/17449))
+- chore(deps): bump astro from 4.16.18 to 4.16.19 in /dev-packages/e2e-tests/test-applications/astro-4 ([#17434](https://github.com/getsentry/sentry-javascript/pull/17434))
+- test(e2e/firebase): Fix firebase e2e test failing due to outdated rules file ([#17448](https://github.com/getsentry/sentry-javascript/pull/17448))
+- test(nextjs): Fix canary tests ([#17416](https://github.com/getsentry/sentry-javascript/pull/17416))
+- test(nuxt): Don't rely on flushing for lowQualityTransactionFilter ([#17406](https://github.com/getsentry/sentry-javascript/pull/17406))
+- test(solidstart): Don't rely on flushing for lowQualityTransactionFilter ([#17408](https://github.com/getsentry/sentry-javascript/pull/17408))
+
+
+
+Work in this release was contributed by @Karibash. Thank you for your contribution!
+
## 10.5.0
- feat(core): better cause data extraction ([#17375](https://github.com/getsentry/sentry-javascript/pull/17375))
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/package.json b/dev-packages/e2e-tests/test-applications/astro-4/package.json
index 742d78cb096c..d355f35e6315 100644
--- a/dev-packages/e2e-tests/test-applications/astro-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/astro-4/package.json
@@ -18,7 +18,7 @@
"@sentry/astro": "* || latest",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@spotlightjs/astro": "2.1.6",
- "astro": "4.16.18",
+ "astro": "4.16.19",
"typescript": "^5.5.4"
},
"devDependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs
new file mode 100644
index 000000000000..53785b6046f7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs
@@ -0,0 +1,3 @@
+export const handler = async () => {
+ throw new Error('test esm');
+};
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
index b13f30397b62..e51d323c1347 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs
@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/aws-serverless';
import * as http from 'node:http';
-export const handler = Sentry.wrapHandler(async () => {
+export const handler = async () => {
await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
await new Promise(resolve => {
http.get('http://example.com', res => {
@@ -16,4 +16,4 @@ export const handler = Sentry.wrapHandler(async () => {
});
});
});
-});
+};
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js
index 534909d6764e..e53b6670225d 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingCjs/index.js
@@ -1,7 +1,7 @@
const http = require('http');
const Sentry = require('@sentry/aws-serverless');
-exports.handler = Sentry.wrapHandler(async () => {
+exports.handler = async () => {
await new Promise(resolve => {
const req = http.request(
{
@@ -21,4 +21,4 @@ exports.handler = Sentry.wrapHandler(async () => {
});
Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {});
-});
+};
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs
index 346613025497..e085a7cc2f8f 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-npm/TracingEsm/index.mjs
@@ -1,7 +1,7 @@
import * as http from 'node:http';
import * as Sentry from '@sentry/aws-serverless';
-export const handler = Sentry.wrapHandler(async () => {
+export const handler = async () => {
await new Promise(resolve => {
const req = http.request(
{
@@ -21,4 +21,4 @@ export const handler = Sentry.wrapHandler(async () => {
});
Sentry.startSpan({ name: 'manual-span', op: 'manual' }, () => {});
-});
+};
diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
index 79ad0fa31070..c20659835ee8 100644
--- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
+++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts
@@ -160,7 +160,35 @@ test.describe('Lambda layer', () => {
type: 'Error',
value: 'test',
mechanism: {
- type: 'auto.function.aws-serverless.handler',
+ type: 'auto.function.aws-serverless.otel',
+ handled: false,
+ },
+ }),
+ );
+ });
+
+ test('capturing errors works in ESM', async ({ lambdaClient }) => {
+ const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'test esm';
+ });
+
+ await lambdaClient.send(
+ new InvokeCommand({
+ FunctionName: 'LayerErrorEsm',
+ Payload: JSON.stringify({}),
+ }),
+ );
+
+ const errorEvent = await errorEventPromise;
+
+ // shows the SDK sent an error event
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]).toEqual(
+ expect.objectContaining({
+ type: 'Error',
+ value: 'test esm',
+ mechanism: {
+ type: 'auto.function.aws-serverless.otel',
handled: false,
},
}),
diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts
index 455e8e395901..6ed994e9006b 100644
--- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts
+++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts
@@ -1,8 +1,6 @@
import * as Sentry from '@sentry/browser';
-// type cast necessary because TS thinks this file is part of the main
-// thread where self is of type `Window` instead of `Worker`
-Sentry.registerWebWorker({ self: self as unknown as Worker });
+Sentry.registerWebWorker({ self });
// Let the main thread know the worker is ready
self.postMessage({
diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts
index 8dfb70b32853..2582bf234c75 100644
--- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts
+++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts
@@ -1,8 +1,6 @@
import * as Sentry from '@sentry/browser';
-// type cast necessary because TS thinks this file is part of the main
-// thread where self is of type `Window` instead of `Worker`
-Sentry.registerWebWorker({ self: self as unknown as Worker });
+Sentry.registerWebWorker({ self });
// Let the main thread know the worker is ready
self.postMessage({
diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts
index d68265c24ab7..7ea35b0cd82d 100644
--- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts
+++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts
@@ -1,8 +1,6 @@
import * as Sentry from '@sentry/browser';
-// type cast necessary because TS thinks this file is part of the main
-// thread where self is of type `Window` instead of `Worker`
-Sentry.registerWebWorker({ self: self as unknown as Worker });
+Sentry.registerWebWorker({ self });
// Let the main thread know the worker is ready
self.postMessage({
diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json
index 4f5edc248c88..41928e7c8bb1 100644
--- a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json
@@ -3,8 +3,9 @@
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
- "skipLibCheck": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
+ "skipLibCheck": false,
+ "skipDefaultLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json
index ef9e351d7a7b..1ed098ed9058 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json
index ef9e351d7a7b..a2672ddb4974 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "es2018",
+ "allowImportingTsExtensions": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json
index bd69196a9ca4..84fbe633ea0b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json
index bd69196a9ca4..84fbe633ea0b 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json
index ef9e351d7a7b..1ed098ed9058 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "allowImportingTsExtensions": true,
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules b/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules
index 260e089a299b..20db64464c57 100644
--- a/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules
+++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore.rules
@@ -3,16 +3,8 @@ rules_version='2'
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
- // This rule allows anyone with your database reference to view, edit,
- // and delete all data in your database. It is useful for getting
- // started, but it is configured to expire after 30 days because it
- // leaves your app open to attackers. At that time, all client
- // requests to your database will be denied.
- //
- // Make sure to write security rules for your app before that time, or
- // else all client requests to your database will be denied until you
- // update your rules.
- allow read, write: if request.time < timestamp.date(2025, 8, 17);
+ // general access within this test app's emulator is fine
+ allow read, write: if 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
new file mode 100644
index 000000000000..9344137a4ed3
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node';
+import { nodeContextIntegration } from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ sendDefaultPii: false,
+ transport: loggingTransport,
+ integrations: [
+ Sentry.anthropicAIIntegration({
+ recordInputs: true,
+ recordOutputs: true,
+ }),
+ nodeContextIntegration(),
+ ],
+});
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
new file mode 100644
index 000000000000..eb8b02b1cf8b
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs
@@ -0,0 +1,15 @@
+import * as Sentry from '@sentry/node';
+import { nodeContextIntegration } from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ transport: loggingTransport,
+ integrations: [
+ Sentry.anthropicAIIntegration(),
+ nodeContextIntegration(),
+ ],
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs
new file mode 100644
index 000000000000..fa011052c50c
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/node';
+import { nodeContextIntegration } from '@sentry/node-core';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ sendDefaultPii: false,
+ transport: loggingTransport,
+ // Force include the integration
+ integrations: [
+ Sentry.anthropicAIIntegration(),
+ nodeContextIntegration(),
+ ],
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs
new file mode 100644
index 000000000000..425d1366879e
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs
@@ -0,0 +1,119 @@
+import { instrumentAnthropicAiClient } from '@sentry/core';
+import * as Sentry from '@sentry/node';
+
+class MockAnthropic {
+ constructor(config) {
+ this.apiKey = config.apiKey;
+
+ // Create messages object with create and countTokens methods
+ this.messages = {
+ create: this._messagesCreate.bind(this),
+ countTokens: this._messagesCountTokens.bind(this)
+ };
+
+ this.models = {
+ retrieve: this._modelsRetrieve.bind(this),
+ };
+ }
+
+ /**
+ * Create a mock message
+ */
+ async _messagesCreate(params) {
+ // Simulate processing time
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ if (params.model === 'error-model') {
+ const error = new Error('Model not found');
+ error.status = 404;
+ error.headers = { 'x-request-id': 'mock-request-123' };
+ throw error;
+ }
+
+ return {
+ id: 'msg_mock123',
+ type: 'message',
+ model: params.model,
+ role: 'assistant',
+ content: [
+ {
+ type: 'text',
+ text: 'Hello from Anthropic mock!',
+ },
+ ],
+ stop_reason: 'end_turn',
+ stop_sequence: null,
+ usage: {
+ input_tokens: 10,
+ output_tokens: 15,
+ },
+ };
+ }
+
+ async _messagesCountTokens() {
+ // Simulate processing time
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ // For countTokens, just return input_tokens
+ return {
+ input_tokens: 15
+ }
+ }
+
+ async _modelsRetrieve(modelId) {
+ // Simulate processing time
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ // Match what the actual implementation would return
+ return {
+ id: modelId,
+ name: modelId,
+ created_at: 1715145600,
+ model: modelId, // Add model field to match the check in addResponseAttributes
+ };
+ }
+}
+
+async function run() {
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
+ const mockClient = new MockAnthropic({
+ apiKey: 'mock-api-key',
+ });
+
+ const client = instrumentAnthropicAiClient(mockClient);
+
+ // First test: basic message completion
+ await client.messages.create({
+ model: 'claude-3-haiku-20240307',
+ system: 'You are a helpful assistant.',
+ messages: [
+ { role: 'user', content: 'What is the capital of France?' },
+ ],
+ temperature: 0.7,
+ max_tokens: 100,
+ });
+
+ // Second test: error handling
+ try {
+ await client.messages.create({
+ model: 'error-model',
+ messages: [{ role: 'user', content: 'This will fail' }],
+ });
+ } catch {
+ // Error is expected and handled
+ }
+
+ // Third test: count tokens with cached tokens
+ await client.messages.countTokens({
+ model: 'claude-3-haiku-20240307',
+ messages: [
+ { role: 'user', content: 'What is the capital of France?' },
+ ],
+ });
+
+ // Fourth test: models.retrieve
+ await client.models.retrieve('claude-3-haiku-20240307');
+ });
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts
new file mode 100644
index 000000000000..4b7d19b7cc58
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts
@@ -0,0 +1,221 @@
+import { afterAll, describe, expect } from 'vitest';
+import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
+
+describe('Anthropic integration', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // First span - basic message completion without PII
+ expect.objectContaining({
+ data: {
+ 'gen_ai.operation.name': 'messages',
+ 'sentry.op': 'gen_ai.messages',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ 'gen_ai.request.temperature': 0.7,
+ 'gen_ai.request.max_tokens': 100,
+ 'gen_ai.response.model': 'claude-3-haiku-20240307',
+ 'gen_ai.response.id': 'msg_mock123',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 15,
+ 'gen_ai.usage.total_tokens': 25,
+ },
+ description: 'messages claude-3-haiku-20240307',
+ op: 'gen_ai.messages',
+ origin: 'auto.ai.anthropic',
+ status: 'ok',
+ }),
+ // Second span - error handling
+ expect.objectContaining({
+ data: {
+ 'gen_ai.operation.name': 'messages',
+ 'sentry.op': 'gen_ai.messages',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'error-model',
+ },
+ description: 'messages error-model',
+ op: 'gen_ai.messages',
+ origin: 'auto.ai.anthropic',
+ status: 'unknown_error',
+ }),
+ // Third span - token counting (no response.text because recordOutputs=false by default)
+ expect.objectContaining({
+ data: {
+ 'gen_ai.operation.name': 'messages',
+ 'sentry.op': 'gen_ai.messages',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ },
+ description: 'messages claude-3-haiku-20240307',
+ op: 'gen_ai.messages',
+ origin: 'auto.ai.anthropic',
+ status: 'ok',
+ }),
+ // Fourth span - models.retrieve
+ expect.objectContaining({
+ data: {
+ 'anthropic.response.timestamp': '2024-05-08T05:20:00.000Z',
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ 'gen_ai.response.id': 'claude-3-haiku-20240307',
+ 'gen_ai.response.model': '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: {
+ 'gen_ai.operation.name': 'messages',
+ 'sentry.op': 'gen_ai.messages',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ 'gen_ai.request.temperature': 0.7,
+ 'gen_ai.request.max_tokens': 100,
+ 'gen_ai.request.messages': '[{"role":"user","content":"What is the capital of France?"}]',
+ 'gen_ai.response.model': 'claude-3-haiku-20240307',
+ 'gen_ai.response.id': 'msg_mock123',
+ 'gen_ai.response.text': 'Hello from Anthropic mock!',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 15,
+ 'gen_ai.usage.total_tokens': 25,
+ },
+ description: 'messages claude-3-haiku-20240307',
+ op: 'gen_ai.messages',
+ origin: 'auto.ai.anthropic',
+ status: 'ok',
+ }),
+ // Second span - error handling with PII
+ expect.objectContaining({
+ data: {
+ 'gen_ai.operation.name': 'messages',
+ 'sentry.op': 'gen_ai.messages',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'error-model',
+ 'gen_ai.request.messages': '[{"role":"user","content":"This will fail"}]',
+ },
+ description: 'messages error-model',
+ op: 'gen_ai.messages',
+ origin: 'auto.ai.anthropic',
+ status: 'unknown_error',
+ }),
+ // Third span - token counting with PII (response.text is present because sendDefaultPii=true enables recordOutputs)
+ expect.objectContaining({
+ data: {
+ 'gen_ai.operation.name': 'messages',
+ 'sentry.op': 'gen_ai.messages',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ 'gen_ai.request.messages': '[{"role":"user","content":"What is the capital of France?"}]',
+ 'gen_ai.response.text': '15', // Only present because recordOutputs=true when sendDefaultPii=true
+ },
+ description: 'messages claude-3-haiku-20240307',
+ op: 'gen_ai.messages',
+ origin: 'auto.ai.anthropic',
+ status: 'ok',
+ }),
+ // Fourth span - models.retrieve with PII
+ expect.objectContaining({
+ data: {
+ 'anthropic.response.timestamp': '2024-05-08T05:20:00.000Z',
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.anthropic',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ 'gen_ai.response.id': 'claude-3-haiku-20240307',
+ 'gen_ai.response.model': 'claude-3-haiku-20240307',
+ },
+ description: 'models claude-3-haiku-20240307',
+ op: 'gen_ai.models',
+ 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.request.messages': expect.any(String), // Should include messages when recordInputs: true
+ 'gen_ai.response.text': expect.any(String), // Should include response text when recordOutputs: true
+ }),
+ }),
+ // Check token counting with options
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'messages',
+ 'gen_ai.request.messages': expect.any(String), // Should include messages when recordInputs: true
+ 'gen_ai.response.text': '15', // Present because recordOutputs=true is set in options
+ }),
+ op: 'gen_ai.messages',
+ }),
+ // Check models.retrieve with options
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'gen_ai.system': 'anthropic',
+ 'gen_ai.request.model': 'claude-3-haiku-20240307',
+ 'gen_ai.response.id': 'claude-3-haiku-20240307',
+ 'gen_ai.response.model': 'claude-3-haiku-20240307',
+ }),
+ op: 'gen_ai.models',
+ description: 'models claude-3-haiku-20240307',
+ }),
+ ]),
+ };
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('creates anthropic related spans with sendDefaultPii: false', async () => {
+ await createRunner()
+ .ignore('event')
+ .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE })
+ .start()
+ .completed();
+ });
+ });
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
+ test('creates anthropic related spans with sendDefaultPii: true', async () => {
+ await createRunner()
+ .ignore('event')
+ .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE })
+ .start()
+ .completed();
+ });
+ });
+
+ createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-options.mjs', (createRunner, test) => {
+ test('creates anthropic related spans with custom options', async () => {
+ await createRunner()
+ .ignore('event')
+ .expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS })
+ .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 720345cc7d86..94fd0dde8486 100644
--- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts
@@ -197,73 +197,6 @@ describe('Vercel AI integration', () => {
]),
};
- // Todo: Add missing attribute spans for v5
- // Right now only second span is recorded as it's manually opted in via explicit telemetry option
- const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_V5 = {
- transaction: 'main',
- spans: expect.arrayContaining([
- expect.objectContaining({
- data: {
- 'vercel.ai.model.id': 'mock-model-id',
- 'vercel.ai.model.provider': 'mock-provider',
- 'vercel.ai.operationId': 'ai.generateText',
- 'vercel.ai.pipeline.name': 'generateText',
- 'vercel.ai.prompt': '{"prompt":"Where is the second span?"}',
- 'vercel.ai.response.finishReason': 'stop',
- 'gen_ai.response.text': expect.any(String),
- 'vercel.ai.settings.maxRetries': 2,
- // 'vercel.ai.settings.maxSteps': 1,
- 'vercel.ai.streaming': false,
- 'gen_ai.prompt': '{"prompt":"Where is the second span?"}',
- 'gen_ai.response.model': 'mock-model-id',
- 'gen_ai.usage.input_tokens': 10,
- 'gen_ai.usage.output_tokens': 20,
- 'gen_ai.usage.total_tokens': 30,
- 'operation.name': 'ai.generateText',
- 'sentry.op': 'gen_ai.invoke_agent',
- 'sentry.origin': 'auto.vercelai.otel',
- },
- description: 'generateText',
- op: 'gen_ai.invoke_agent',
- origin: 'auto.vercelai.otel',
- status: 'ok',
- }),
- // doGenerate
- expect.objectContaining({
- data: {
- 'sentry.origin': 'auto.vercelai.otel',
- 'sentry.op': 'gen_ai.generate_text',
- 'operation.name': 'ai.generateText.doGenerate',
- 'vercel.ai.operationId': 'ai.generateText.doGenerate',
- 'vercel.ai.model.provider': 'mock-provider',
- 'vercel.ai.model.id': 'mock-model-id',
- 'vercel.ai.settings.maxRetries': 2,
- 'gen_ai.system': 'mock-provider',
- 'gen_ai.request.model': 'mock-model-id',
- '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.response.text': 'Second span here!',
- 'vercel.ai.response.timestamp': expect.any(String),
- // 'vercel.ai.prompt.format': expect.any(String),
- 'gen_ai.request.messages': expect.any(String),
- 'gen_ai.response.finish_reasons': ['stop'],
- 'gen_ai.usage.input_tokens': 10,
- 'gen_ai.usage.output_tokens': 20,
- 'gen_ai.response.id': expect.any(String),
- 'gen_ai.response.model': 'mock-model-id',
- 'gen_ai.usage.total_tokens': 30,
- },
- description: 'generate_text mock-model-id',
- op: 'gen_ai.generate_text',
- origin: 'auto.vercelai.otel',
- status: 'ok',
- }),
- ]),
- };
-
const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = {
transaction: 'main',
spans: expect.arrayContaining([
@@ -605,23 +538,6 @@ describe('Vercel AI integration', () => {
});
});
- // Test with specific Vercel AI v5 version
- createEsmAndCjsTests(
- __dirname,
- 'scenario-v5.mjs',
- 'instrument.mjs',
- (createRunner, test) => {
- test('creates ai related spans with v5', async () => {
- await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_V5 }).start().completed();
- });
- },
- {
- additionalDependencies: {
- ai: '^5.0.0',
- },
- },
- );
-
createEsmAndCjsTests(__dirname, 'scenario-error-in-tool-express.mjs', 'instrument.mjs', (createRunner, test) => {
test('captures error in tool in express server', async () => {
const expectedTransaction = {
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
new file mode 100644
index 000000000000..b798e21228f5
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/instrument-with-pii.mjs
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/node';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ transport: loggingTransport,
+ integrations: [Sentry.vercelAIIntegration()],
+});
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
new file mode 100644
index 000000000000..5e898ee1949d
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/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',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.vercelAIIntegration()],
+});
diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario-error-in-tool.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario-error-in-tool.mjs
new file mode 100644
index 000000000000..9ba3ac4b7d4a
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario-error-in-tool.mjs
@@ -0,0 +1,36 @@
+import * as Sentry from '@sentry/node';
+import { generateText, tool } from 'ai';
+import { MockLanguageModelV2 } from 'ai/test';
+import { z } from 'zod';
+
+async function run() {
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
+ await generateText({
+ model: new MockLanguageModelV2({
+ doGenerate: async () => ({
+ finishReason: 'tool-calls',
+ usage: { inputTokens: 15, outputTokens: 25, totalTokens: 40 },
+ content: [
+ {
+ type: 'tool-call',
+ toolCallId: 'call-1',
+ toolName: 'getWeather',
+ input: JSON.stringify({ location: 'San Francisco' }),
+ },
+ ],
+ }),
+ }),
+ tools: {
+ getWeather: tool({
+ inputSchema: z.object({ location: z.string() }),
+ execute: async () => {
+ throw new Error('Error in tool');
+ },
+ }),
+ },
+ prompt: 'What is the weather in San Francisco?',
+ });
+ });
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-v5.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs
similarity index 82%
rename from dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-v5.mjs
rename to dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs
index 8cfe6d64ad05..9ef1b8000741 100644
--- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-v5.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/node';
-import { generateText } from 'ai';
+import { generateText, tool } from 'ai';
import { MockLanguageModelV2 } from 'ai/test';
import { z } from 'zod';
@@ -35,24 +35,21 @@ async function run() {
doGenerate: async () => ({
finishReason: 'tool-calls',
usage: { inputTokens: 15, outputTokens: 25, totalTokens: 40 },
- content: [{ type: 'text', text: 'Tool call completed!' }],
- toolCalls: [
+ content: [
{
- toolCallType: 'function',
+ type: 'tool-call',
toolCallId: 'call-1',
toolName: 'getWeather',
- args: '{ "location": "San Francisco" }',
+ input: JSON.stringify({ location: 'San Francisco' }),
},
],
}),
}),
tools: {
- getWeather: {
- parameters: z.object({ location: z.string() }),
- execute: async args => {
- return `Weather in ${args.location}: Sunny, 72°F`;
- },
- },
+ getWeather: tool({
+ inputSchema: z.object({ location: z.string() }),
+ execute: async ({ location }) => `Weather in ${location}: Sunny, 72°F`,
+ }),
},
prompt: 'What is the weather in San Francisco?',
});
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
new file mode 100644
index 000000000000..470080658dfa
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts
@@ -0,0 +1,564 @@
+import type { Event } from '@sentry/node';
+import { afterAll, describe, expect } from 'vitest';
+import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner';
+
+describe('Vercel AI integration (V5)', () => {
+ afterAll(() => {
+ 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: {
+ 'vercel.ai.model.id': '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': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ 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: {
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'vercel.ai.operationId': 'ai.generateText.doGenerate',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.settings.maxRetries': 2,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ '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': ['stop'],
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.total_tokens': 30,
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Third span - explicit telemetry enabled, should record inputs/outputs regardless of sendDefaultPii
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateText',
+ 'vercel.ai.pipeline.name': 'generateText',
+ 'vercel.ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'vercel.ai.response.finishReason': 'stop',
+ 'gen_ai.response.text': expect.any(String),
+ 'vercel.ai.settings.maxRetries': 2,
+ 'vercel.ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'gen_ai.invoke_agent',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Fourth span - doGenerate for explicit telemetry enabled call
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'vercel.ai.operationId': 'ai.generateText.doGenerate',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.settings.maxRetries': 2,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ '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.response.text': expect.any(String),
+ 'vercel.ai.response.timestamp': expect.any(String),
+ 'gen_ai.request.messages': expect.any(String),
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.total_tokens': 30,
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Fifth span - tool call generateText span
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.model.id': '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': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'gen_ai.invoke_agent',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Sixth span - tool call doGenerate span
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.model.id': '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.request.model': 'mock-model-id',
+ 'gen_ai.response.finish_reasons': ['tool-calls'],
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Seventh span - tool call execution span
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.operationId': 'ai.toolCall',
+ 'gen_ai.tool.call.id': 'call-1',
+ 'gen_ai.tool.name': 'getWeather',
+ 'gen_ai.tool.type': 'function',
+ 'operation.name': 'ai.toolCall',
+ 'sentry.op': 'gen_ai.execute_tool',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'execute_tool getWeather',
+ op: 'gen_ai.execute_tool',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ ]),
+ };
+
+ 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: {
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateText',
+ 'vercel.ai.pipeline.name': 'generateText',
+ 'vercel.ai.prompt': '{"prompt":"Where is the first span?"}',
+ 'vercel.ai.response.finishReason': 'stop',
+ 'gen_ai.response.text': 'First span here!',
+ 'vercel.ai.settings.maxRetries': 2,
+ 'vercel.ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"Where is the first span?"}',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ 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: {
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateText.doGenerate',
+ 'vercel.ai.pipeline.name': 'generateText.doGenerate',
+ 'gen_ai.request.messages': '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]',
+ 'vercel.ai.response.finishReason': 'stop',
+ 'vercel.ai.response.id': expect.any(String),
+ 'vercel.ai.response.model': 'mock-model-id',
+ 'gen_ai.response.text': 'First span here!',
+ 'vercel.ai.response.timestamp': expect.any(String),
+ 'vercel.ai.settings.maxRetries': 2,
+ 'vercel.ai.streaming': false,
+ 'gen_ai.request.model': 'mock-model-id',
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Third span - explicitly enabled telemetry, should record inputs/outputs regardless of sendDefaultPii
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateText',
+ 'vercel.ai.pipeline.name': 'generateText',
+ 'vercel.ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'vercel.ai.response.finishReason': 'stop',
+ 'gen_ai.response.text': expect.any(String),
+ 'vercel.ai.settings.maxRetries': 2,
+ 'vercel.ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"Where is the second span?"}',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.usage.total_tokens': 30,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ op: 'gen_ai.invoke_agent',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Fourth span - doGenerate for explicitly enabled telemetry call
+ expect.objectContaining({
+ data: {
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'vercel.ai.operationId': 'ai.generateText.doGenerate',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.settings.maxRetries': 2,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ '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.response.text': expect.any(String),
+ 'vercel.ai.response.timestamp': expect.any(String),
+ 'gen_ai.request.messages': expect.any(String),
+ 'gen_ai.response.finish_reasons': ['stop'],
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 20,
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.total_tokens': 30,
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Fifth span - tool call generateText span (should include prompts when sendDefaultPii: true)
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateText',
+ 'vercel.ai.pipeline.name': 'generateText',
+ 'vercel.ai.prompt': '{"prompt":"What is the weather in San Francisco?"}',
+ 'vercel.ai.response.finishReason': 'tool-calls',
+ // 'gen_ai.response.text': 'Tool call completed!',
+ 'gen_ai.response.tool_calls': expect.any(String),
+ 'vercel.ai.settings.maxRetries': 2,
+ 'vercel.ai.streaming': false,
+ 'gen_ai.prompt': '{"prompt":"What is the weather in San Francisco?"}',
+ '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,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generateText',
+ 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: {
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateText.doGenerate',
+ 'vercel.ai.pipeline.name': 'generateText.doGenerate',
+ 'gen_ai.request.messages': expect.any(String),
+ 'vercel.ai.prompt.toolChoice': expect.any(String),
+ 'gen_ai.request.available_tools': expect.any(Array),
+ 'vercel.ai.response.finishReason': 'tool-calls',
+ 'vercel.ai.response.id': expect.any(String),
+ 'vercel.ai.response.model': 'mock-model-id',
+ // 'gen_ai.response.text': 'Tool call completed!', // TODO: look into why this is not being set
+ 'vercel.ai.response.timestamp': expect.any(String),
+ 'gen_ai.response.tool_calls': expect.any(String),
+ 'vercel.ai.settings.maxRetries': 2,
+ 'vercel.ai.streaming': false,
+ 'gen_ai.request.model': 'mock-model-id',
+ 'gen_ai.response.finish_reasons': ['tool-calls'],
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // Seventh span - tool call execution span
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.operationId': 'ai.toolCall',
+ 'gen_ai.tool.call.id': 'call-1',
+ 'gen_ai.tool.name': 'getWeather',
+ 'gen_ai.tool.input': expect.any(String),
+ 'gen_ai.tool.output': expect.any(String),
+ 'gen_ai.tool.type': 'function',
+ 'operation.name': 'ai.toolCall',
+ 'sentry.op': 'gen_ai.execute_tool',
+ '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();
+ });
+ },
+ {
+ additionalDependencies: {
+ ai: '^5.0.0',
+ },
+ },
+ );
+
+ 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();
+ });
+ },
+ {
+ additionalDependencies: {
+ ai: '^5.0.0',
+ },
+ },
+ );
+
+ 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: {
+ 'vercel.ai.model.id': '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': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ 'operation.name': 'ai.generateText',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'vercel.ai.response.finishReason': 'tool-calls',
+ },
+ description: 'generateText',
+ op: 'gen_ai.invoke_agent',
+ origin: 'auto.vercelai.otel',
+ }),
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.model.id': '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.request.model': 'mock-model-id',
+ 'gen_ai.response.finish_reasons': ['tool-calls'],
+ 'gen_ai.response.id': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ 'operation.name': 'ai.generateText.doGenerate',
+ 'sentry.op': 'gen_ai.generate_text',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'generate_text mock-model-id',
+ op: 'gen_ai.generate_text',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ expect.objectContaining({
+ data: {
+ 'vercel.ai.operationId': 'ai.toolCall',
+ 'gen_ai.tool.call.id': 'call-1',
+ 'gen_ai.tool.name': 'getWeather',
+ 'gen_ai.tool.type': 'function',
+ 'operation.name': 'ai.toolCall',
+ 'sentry.op': 'gen_ai.execute_tool',
+ 'sentry.origin': 'auto.vercelai.otel',
+ },
+ description: 'execute_tool getWeather',
+ op: 'gen_ai.execute_tool',
+ origin: 'auto.vercelai.otel',
+ status: 'unknown_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;
+
+ await createRunner()
+ .expect({
+ transaction: transaction => {
+ transactionEvent = transaction;
+ },
+ })
+ .expect({
+ event: event => {
+ errorEvent = event;
+ },
+ })
+ .start()
+ .completed();
+
+ expect(transactionEvent).toBeDefined();
+ expect(transactionEvent).toMatchObject(expectedTransaction);
+
+ expect(errorEvent).toBeDefined();
+ expect(errorEvent).toMatchObject(expectedError);
+
+ // Trace id should be the same for the transaction and error event
+ expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id);
+ });
+ },
+ {
+ additionalDependencies: {
+ ai: '^5.0.0',
+ },
+ },
+ );
+
+ createEsmAndCjsTests(
+ __dirname,
+ 'scenario.mjs',
+ 'instrument.mjs',
+ (createRunner, test) => {
+ test('creates ai related spans with v5', async () => {
+ await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed();
+ });
+ },
+ {
+ additionalDependencies: {
+ ai: '^5.0.0',
+ },
+ },
+ );
+});
diff --git a/packages/astro/.eslintrc.cjs b/packages/astro/.eslintrc.cjs
index 29b78099e7c6..3be941649fcf 100644
--- a/packages/astro/.eslintrc.cjs
+++ b/packages/astro/.eslintrc.cjs
@@ -8,7 +8,7 @@ module.exports = {
{
files: ['vite.config.ts'],
parserOptions: {
- project: ['tsconfig.test.json'],
+ project: ['tsconfig.vite.json'],
},
},
],
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 3b2f589f7fc2..ce9a1b1fa65a 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -12,6 +12,7 @@ export {
addEventProcessor,
addIntegration,
amqplibIntegration,
+ anthropicAIIntegration,
// eslint-disable-next-line deprecation/deprecation
anrIntegration,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts
index 1a9eeaff8cd4..29dcc15ade0c 100644
--- a/packages/astro/src/integration/index.ts
+++ b/packages/astro/src/integration/index.ts
@@ -27,8 +27,13 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
clientInitPath,
serverInitPath,
autoInstrumentation,
+ // eslint-disable-next-line deprecation/deprecation
sourceMapsUploadOptions,
+ sourcemaps,
+ // todo(v11): Extract `release` build time option here - cannot be done currently, because it conflicts with the `DeprecatedRuntimeOptions` type
+ // release,
bundleSizeOptimizations,
+ unstable_sentryVitePluginOptions,
debug,
...otherOptions
} = options;
@@ -48,8 +53,21 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
};
const sourceMapsNeeded = sdkEnabled.client || sdkEnabled.server;
- const { unstable_sentryVitePluginOptions, ...uploadOptions } = sourceMapsUploadOptions || {};
- const shouldUploadSourcemaps = (sourceMapsNeeded && uploadOptions?.enabled) ?? true;
+ // eslint-disable-next-line deprecation/deprecation
+ const { unstable_sentryVitePluginOptions: deprecatedVitePluginOptions, ...uploadOptions } =
+ sourceMapsUploadOptions || {};
+
+ const unstableMerged_sentryVitePluginOptions = {
+ ...deprecatedVitePluginOptions,
+ ...unstable_sentryVitePluginOptions,
+ };
+
+ const shouldUploadSourcemaps =
+ (sourceMapsNeeded &&
+ sourcemaps?.disable !== true &&
+ // eslint-disable-next-line deprecation/deprecation
+ uploadOptions?.enabled) ??
+ true;
// We don't need to check for AUTH_TOKEN here, because the plugin will pick it up from the env
if (shouldUploadSourcemaps && command !== 'dev') {
@@ -58,7 +76,9 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
let updatedFilesToDeleteAfterUpload: string[] | undefined = undefined;
if (
+ // eslint-disable-next-line deprecation/deprecation
typeof uploadOptions?.filesToDeleteAfterUpload === 'undefined' &&
+ typeof sourcemaps?.filesToDeleteAfterUpload === 'undefined' &&
computedSourceMapSettings.previousUserSourceMapSetting === 'unset'
) {
// This also works for adapters, as the source maps are also copied to e.g. the .vercel folder
@@ -79,26 +99,40 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
},
plugins: [
sentryVitePlugin({
- org: uploadOptions.org ?? env.SENTRY_ORG,
- project: uploadOptions.project ?? env.SENTRY_PROJECT,
- authToken: uploadOptions.authToken ?? env.SENTRY_AUTH_TOKEN,
- telemetry: uploadOptions.telemetry ?? true,
+ // Priority: top-level options > deprecated options > env vars
+ // eslint-disable-next-line deprecation/deprecation
+ org: options.org ?? uploadOptions.org ?? env.SENTRY_ORG,
+ // eslint-disable-next-line deprecation/deprecation
+ project: options.project ?? uploadOptions.project ?? env.SENTRY_PROJECT,
+ // eslint-disable-next-line deprecation/deprecation
+ authToken: options.authToken ?? uploadOptions.authToken ?? env.SENTRY_AUTH_TOKEN,
+ url: options.sentryUrl ?? env.SENTRY_URL,
+ headers: options.headers,
+ // eslint-disable-next-line deprecation/deprecation
+ telemetry: options.telemetry ?? uploadOptions.telemetry ?? true,
+ silent: options.silent ?? false,
+ errorHandler: options.errorHandler,
_metaOptions: {
telemetry: {
metaFramework: 'astro',
},
},
- ...unstable_sentryVitePluginOptions,
- debug: debug ?? false,
+ ...unstableMerged_sentryVitePluginOptions,
+ debug: options.debug ?? false,
sourcemaps: {
- assets: uploadOptions.assets ?? [getSourcemapsAssetsGlob(config)],
+ ...options.sourcemaps,
+ // eslint-disable-next-line deprecation/deprecation
+ assets: sourcemaps?.assets ?? uploadOptions.assets ?? [getSourcemapsAssetsGlob(config)],
filesToDeleteAfterUpload:
- uploadOptions?.filesToDeleteAfterUpload ?? updatedFilesToDeleteAfterUpload,
- ...unstable_sentryVitePluginOptions?.sourcemaps,
+ sourcemaps?.filesToDeleteAfterUpload ??
+ // eslint-disable-next-line deprecation/deprecation
+ uploadOptions?.filesToDeleteAfterUpload ??
+ updatedFilesToDeleteAfterUpload,
+ ...unstableMerged_sentryVitePluginOptions?.sourcemaps,
},
bundleSizeOptimizations: {
...bundleSizeOptimizations,
- ...unstable_sentryVitePluginOptions?.bundleSizeOptimizations,
+ ...unstableMerged_sentryVitePluginOptions?.bundleSizeOptimizations,
},
}),
],
diff --git a/packages/astro/src/integration/types.ts b/packages/astro/src/integration/types.ts
index aed2b7e1d193..ec9996cba134 100644
--- a/packages/astro/src/integration/types.ts
+++ b/packages/astro/src/integration/types.ts
@@ -1,3 +1,4 @@
+import type { BuildTimeOptionsBase, UnstableVitePluginOptions } from '@sentry/core';
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
import type { RouteData } from 'astro';
@@ -23,12 +24,16 @@ type SdkInitPaths = {
serverInitPath?: string;
};
+/**
+ * @deprecated Move these options to the top-level of your Sentry configuration.
+ */
type SourceMapsOptions = {
/**
* If this flag is `true`, and an auth token is detected, the Sentry integration will
* automatically generate and upload source maps to Sentry during a production build.
*
* @default true
+ * @deprecated Use `sourcemaps.disable` instead (with inverted logic)
*/
enabled?: boolean;
@@ -39,18 +44,24 @@ type SourceMapsOptions = {
*
* To create an auth token, follow this guide:
* @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens
+ *
+ * @deprecated Use top-level `authToken` option instead
*/
authToken?: string;
/**
* The organization slug of your Sentry organization.
* Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable.
+ *
+ * @deprecated Use top-level `org` option instead
*/
org?: string;
/**
* The project slug of your Sentry project.
* Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable.
+ *
+ * @deprecated Use top-level `project` option instead
*/
project?: string;
@@ -59,6 +70,7 @@ type SourceMapsOptions = {
* It will not collect any sensitive or user-specific data.
*
* @default true
+ * @deprecated Use top-level `telemetry` option instead
*/
telemetry?: boolean;
@@ -71,6 +83,8 @@ type SourceMapsOptions = {
*
* The globbing patterns must follow the implementation of the `glob` package.
* @see https://www.npmjs.com/package/glob#glob-primer
+ *
+ * @deprecated Use `sourcemaps.assets` instead
*/
assets?: string | Array;
@@ -81,6 +95,8 @@ type SourceMapsOptions = {
* @default [] - By default no files are deleted.
*
* The globbing patterns follow the implementation of the glob package. (https://www.npmjs.com/package/glob)
+ *
+ * @deprecated Use `sourcemaps.filesToDeleteAfterUpload` instead
*/
filesToDeleteAfterUpload?: string | Array;
@@ -95,49 +111,10 @@ type SourceMapsOptions = {
* changes can occur at any time within a major SDK version.
*
* Furthermore, some options are untested with Astro specifically. Use with caution.
- */
- unstable_sentryVitePluginOptions?: Partial;
-};
-
-type BundleSizeOptimizationOptions = {
- /**
- * If set to `true`, the plugin will attempt to tree-shake (remove) any debugging code within the Sentry SDK.
- * Note that the success of this depends on tree shaking being enabled in your build tooling.
- *
- * Setting this option to `true` will disable features like the SDK's `debug` option.
- */
- excludeDebugStatements?: boolean;
-
- /**
- * If set to true, the plugin will try to tree-shake performance monitoring statements out.
- * Note that the success of this depends on tree shaking generally being enabled in your build.
- * Attention: DO NOT enable this when you're using any performance monitoring-related SDK features (e.g. Sentry.startSpan()).
- */
- excludeTracing?: boolean;
-
- /**
- * If set to `true`, the plugin will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay Shadow DOM recording functionality.
- * Note that the success of this depends on tree shaking being enabled in your build tooling.
*
- * This option is safe to be used when you do not want to capture any Shadow DOM activity via Sentry Session Replay.
+ * @deprecated Use top-level `unstable_sentryVitePluginOptions` instead
*/
- excludeReplayShadowDom?: boolean;
-
- /**
- * If set to `true`, the plugin will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay `iframe` recording functionality.
- * Note that the success of this depends on tree shaking being enabled in your build tooling.
- *
- * You can safely do this when you do not want to capture any `iframe` activity via Sentry Session Replay.
- */
- excludeReplayIframe?: boolean;
-
- /**
- * If set to `true`, the plugin will attempt to tree-shake (remove) code related to the Sentry SDK's Session Replay's Compression Web Worker.
- * Note that the success of this depends on tree shaking being enabled in your build tooling.
- *
- * **Notice:** You should only do use this option if you manually host a compression worker and configure it in your Sentry Session Replay integration config via the `workerUrl` option.
- */
- excludeReplayWorker?: boolean;
+ unstable_sentryVitePluginOptions?: Partial;
};
type InstrumentationOptions = {
@@ -202,7 +179,10 @@ type DeprecatedRuntimeOptions = Record;
*
* If you specify a dedicated init file, the SDK options passed to `sentryAstro` will be ignored.
*/
-export type SentryOptions = SdkInitPaths &
+export type SentryOptions = Omit &
+ // todo(v11): `release` and `debug` need to be removed from BuildTimeOptionsBase as it is currently conflicting with `DeprecatedRuntimeOptions`
+ UnstableVitePluginOptions &
+ SdkInitPaths &
InstrumentationOptions &
SdkEnabledOptions & {
/**
@@ -210,19 +190,12 @@ export type SentryOptions = SdkInitPaths &
*
* These options are always read from the `sentryAstro` integration.
* Do not define them in the `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files.
- */
- sourceMapsUploadOptions?: SourceMapsOptions;
- /**
- * Options for the Sentry Vite plugin to customize bundle size optimizations.
*
- * These options are always read from the `sentryAstro` integration.
- * Do not define them in the `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files.
+ * @deprecated This option was deprecated. Please move the options to the top-level configuration.
+ * See the migration guide in the SourceMapsOptions type documentation.
*/
- bundleSizeOptimizations?: BundleSizeOptimizationOptions;
- /**
- * If enabled, prints debug logs during the build process.
- */
- debug?: boolean;
+ // eslint-disable-next-line deprecation/deprecation
+ sourceMapsUploadOptions?: SourceMapsOptions;
// eslint-disable-next-line deprecation/deprecation
} & DeprecatedRuntimeOptions;
diff --git a/packages/astro/test/buildOptions.test-d.ts b/packages/astro/test/buildOptions.test-d.ts
new file mode 100644
index 000000000000..ec4c9c5330f7
--- /dev/null
+++ b/packages/astro/test/buildOptions.test-d.ts
@@ -0,0 +1,190 @@
+import { describe, expectTypeOf, it } from 'vitest';
+import type { SentryOptions } from '../src/integration/types';
+
+describe('Sentry Astro build-time options type', () => {
+ it('includes all options based on type BuildTimeOptionsBase', () => {
+ const completeOptions: SentryOptions = {
+ // --- BuildTimeOptionsBase options ---
+ org: 'test-org',
+ project: 'test-project',
+ authToken: 'test-auth-token',
+ sentryUrl: 'https://sentry.io',
+ headers: { Authorization: ' Bearer test-auth-token' },
+ telemetry: true,
+ silent: false,
+ // eslint-disable-next-line no-console
+ errorHandler: (err: Error) => console.warn(err),
+ debug: false,
+ sourcemaps: {
+ disable: false,
+ assets: ['./dist/**/*'],
+ ignore: ['./dist/*.map'],
+ filesToDeleteAfterUpload: ['./dist/*.map'],
+ },
+ release: {
+ name: 'test-release-1.0.0',
+ create: true,
+ finalize: true,
+ dist: 'test-dist',
+ vcsRemote: 'origin',
+ setCommits: {
+ auto: false,
+ repo: 'test/repo',
+ commit: 'abc123',
+ previousCommit: 'def456',
+ ignoreMissing: false,
+ ignoreEmpty: false,
+ },
+ deploy: {
+ env: 'production',
+ started: 1234567890,
+ finished: 1234567900,
+ time: 10,
+ name: 'deployment-name',
+ url: 'https://example.com',
+ },
+ },
+ bundleSizeOptimizations: {
+ excludeDebugStatements: true,
+ excludeTracing: false,
+ excludeReplayShadowDom: true,
+ excludeReplayIframe: true,
+ excludeReplayWorker: true,
+ },
+
+ // --- UnstableVitePluginOptions ---
+ unstable_sentryVitePluginOptions: {
+ sourcemaps: {
+ assets: './dist/**/*',
+ },
+ bundleSizeOptimizations: {
+ excludeDebugStatements: true,
+ },
+ },
+
+ // --- SentryOptions specific options ---
+ enabled: true,
+ clientInitPath: './src/sentry.client.config.ts',
+ serverInitPath: './src/sentry.server.config.ts',
+ autoInstrumentation: {
+ requestHandler: true,
+ },
+
+ // Deprecated runtime options
+ environment: 'test',
+ dsn: 'https://test@sentry.io/123',
+ sampleRate: 1.0,
+ tracesSampleRate: 1.0,
+ replaysSessionSampleRate: 0.1,
+ replaysOnErrorSampleRate: 1.0,
+ };
+
+ expectTypeOf(completeOptions).toEqualTypeOf();
+ });
+
+ it('includes all deprecated options', () => {
+ const completeOptions: SentryOptions = {
+ // SentryOptions specific options
+ enabled: true,
+ debug: true,
+ clientInitPath: './src/sentry.client.config.ts',
+ serverInitPath: './src/sentry.server.config.ts',
+ autoInstrumentation: {
+ requestHandler: true,
+ },
+ unstable_sentryVitePluginOptions: {
+ sourcemaps: {
+ assets: './dist/**/*',
+ },
+ bundleSizeOptimizations: {
+ excludeDebugStatements: true,
+ },
+ },
+
+ // Deprecated sourceMapsUploadOptions
+ sourceMapsUploadOptions: {
+ enabled: true,
+ authToken: 'deprecated-token',
+ org: 'deprecated-org',
+ project: 'deprecated-project',
+ telemetry: false,
+ assets: './build/**/*',
+ filesToDeleteAfterUpload: ['./build/*.map'],
+ unstable_sentryVitePluginOptions: {
+ sourcemaps: {
+ ignore: ['./build/*.spec.js'],
+ },
+ },
+ },
+ };
+
+ expectTypeOf(completeOptions).toEqualTypeOf();
+ });
+
+ it('allows partial configuration', () => {
+ const minimalOptions: SentryOptions = { enabled: true };
+
+ expectTypeOf(minimalOptions).toEqualTypeOf();
+
+ const partialOptions: SentryOptions = {
+ enabled: true,
+ debug: false,
+ org: 'my-org',
+ project: 'my-project',
+ };
+
+ expectTypeOf(partialOptions).toEqualTypeOf();
+ });
+
+ it('supports BuildTimeOptionsBase options at top level', () => {
+ const baseOptions: SentryOptions = {
+ // Test that all BuildTimeOptionsBase options are available at top level
+ org: 'test-org',
+ project: 'test-project',
+ authToken: 'test-token',
+ sentryUrl: 'https://custom.sentry.io',
+ headers: { 'Custom-Header': 'value' },
+ telemetry: false,
+ silent: true,
+ debug: true,
+ sourcemaps: {
+ disable: false,
+ assets: ['./dist/**/*.js'],
+ ignore: ['./dist/test/**/*'],
+ filesToDeleteAfterUpload: ['./dist/**/*.map'],
+ },
+ release: {
+ name: '1.0.0',
+ create: true,
+ finalize: false,
+ },
+ bundleSizeOptimizations: {
+ excludeDebugStatements: true,
+ excludeTracing: true,
+ },
+ };
+
+ expectTypeOf(baseOptions).toEqualTypeOf();
+ });
+
+ it('supports UnstableVitePluginOptions at top level', () => {
+ const viteOptions: SentryOptions = {
+ unstable_sentryVitePluginOptions: {
+ org: 'override-org',
+ project: 'override-project',
+ sourcemaps: {
+ assets: './custom-dist/**/*',
+ ignore: ['./custom-dist/ignore/**/*'],
+ },
+ bundleSizeOptimizations: {
+ excludeDebugStatements: true,
+ excludeTracing: false,
+ },
+ debug: true,
+ silent: false,
+ },
+ };
+
+ expectTypeOf(viteOptions).toEqualTypeOf();
+ });
+});
diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts
index f7c0f2ec9e14..abb3f48dcf72 100644
--- a/packages/astro/test/integration/index.test.ts
+++ b/packages/astro/test/integration/index.test.ts
@@ -20,6 +20,10 @@ const injectScript = vi.fn();
const config = {
root: new URL('file://path/to/project'),
outDir: new URL('file://path/to/project/out'),
+} as AstroConfig;
+
+const baseConfigHookObject = {
+ logger: { warn: vi.fn(), info: vi.fn() },
};
describe('sentryAstro integration', () => {
@@ -39,7 +43,7 @@ describe('sentryAstro integration', () => {
expect(integration.hooks['astro:config:setup']).toBeDefined();
// @ts-expect-error - the hook exists and we only need to pass what we actually use
- await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config });
+ await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config });
expect(updateConfig).toHaveBeenCalledTimes(1);
expect(updateConfig).toHaveBeenCalledWith({
@@ -52,23 +56,25 @@ describe('sentryAstro integration', () => {
});
expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1);
- expect(sentryVitePluginSpy).toHaveBeenCalledWith({
- authToken: 'my-token',
- org: 'my-org',
- project: 'my-project',
- telemetry: false,
- debug: false,
- bundleSizeOptimizations: {},
- sourcemaps: {
- assets: ['out/**/*'],
- filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
- },
- _metaOptions: {
- telemetry: {
- metaFramework: 'astro',
+ expect(sentryVitePluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ authToken: 'my-token',
+ org: 'my-org',
+ project: 'my-project',
+ telemetry: false,
+ debug: false,
+ bundleSizeOptimizations: {},
+ sourcemaps: {
+ assets: ['out/**/*'],
+ filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
},
- },
- });
+ _metaOptions: {
+ telemetry: {
+ metaFramework: 'astro',
+ },
+ },
+ }),
+ );
});
it('falls back to default output dir, if out and root dir are not available', async () => {
@@ -76,26 +82,28 @@ describe('sentryAstro integration', () => {
sourceMapsUploadOptions: { enabled: true, org: 'my-org', project: 'my-project', telemetry: false },
});
// @ts-expect-error - the hook exists and we only need to pass what we actually use
- await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config: {} });
+ await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config: {} });
expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1);
- expect(sentryVitePluginSpy).toHaveBeenCalledWith({
- authToken: 'my-token',
- org: 'my-org',
- project: 'my-project',
- telemetry: false,
- debug: false,
- bundleSizeOptimizations: {},
- sourcemaps: {
- assets: ['dist/**/*'],
- filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
- },
- _metaOptions: {
- telemetry: {
- metaFramework: 'astro',
+ expect(sentryVitePluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ authToken: 'my-token',
+ org: 'my-org',
+ project: 'my-project',
+ telemetry: false,
+ debug: false,
+ bundleSizeOptimizations: {},
+ sourcemaps: {
+ assets: ['dist/**/*'],
+ filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
},
- },
- });
+ _metaOptions: {
+ telemetry: {
+ metaFramework: 'astro',
+ },
+ },
+ }),
+ );
});
it('sets the correct assets glob for vercel if the Vercel adapter is used', async () => {
@@ -104,6 +112,7 @@ describe('sentryAstro integration', () => {
});
// @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: {
@@ -113,23 +122,25 @@ describe('sentryAstro integration', () => {
});
expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1);
- expect(sentryVitePluginSpy).toHaveBeenCalledWith({
- authToken: 'my-token',
- org: 'my-org',
- project: 'my-project',
- telemetry: false,
- debug: false,
- bundleSizeOptimizations: {},
- sourcemaps: {
- assets: ['{.vercel,dist}/**/*'],
- filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
- },
- _metaOptions: {
- telemetry: {
- metaFramework: 'astro',
+ expect(sentryVitePluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ authToken: 'my-token',
+ org: 'my-org',
+ project: 'my-project',
+ telemetry: false,
+ debug: false,
+ bundleSizeOptimizations: {},
+ sourcemaps: {
+ assets: ['{.vercel,dist}/**/*'],
+ filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
},
- },
- });
+ _metaOptions: {
+ telemetry: {
+ metaFramework: 'astro',
+ },
+ },
+ }),
+ );
});
it('prefers user-specified assets-globs over the default values', async () => {
@@ -143,6 +154,7 @@ describe('sentryAstro integration', () => {
});
// @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,
// @ts-expect-error - only passing in partial config
@@ -152,23 +164,25 @@ describe('sentryAstro integration', () => {
});
expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1);
- expect(sentryVitePluginSpy).toHaveBeenCalledWith({
- authToken: 'my-token',
- org: 'my-org',
- project: 'my-project',
- telemetry: true,
- debug: false,
- bundleSizeOptimizations: {},
- sourcemaps: {
- assets: ['dist/server/**/*, dist/client/**/*'],
- filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
- },
- _metaOptions: {
- telemetry: {
- metaFramework: 'astro',
+ expect(sentryVitePluginSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ authToken: 'my-token',
+ org: 'my-org',
+ project: 'my-project',
+ telemetry: true,
+ debug: false,
+ bundleSizeOptimizations: {},
+ sourcemaps: {
+ assets: ['dist/server/**/*, dist/client/**/*'],
+ filesToDeleteAfterUpload: ['./dist/**/client/**/*.map', './dist/**/server/**/*.map'],
},
- },
- });
+ _metaOptions: {
+ telemetry: {
+ metaFramework: 'astro',
+ },
+ },
+ }),
+ );
});
it('prefers user-specified filesToDeleteAfterUpload over the default values', async () => {
@@ -182,6 +196,7 @@ describe('sentryAstro integration', () => {
});
// @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,
// @ts-expect-error - only passing in partial config
@@ -226,6 +241,7 @@ describe('sentryAstro integration', () => {
});
// @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,
// @ts-expect-error - only passing in partial config
@@ -260,12 +276,36 @@ describe('sentryAstro integration', () => {
expect(integration.hooks['astro:config:setup']).toBeDefined();
// @ts-expect-error - the hook exists and we only need to pass what we actually use
- await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config });
+ await integration.hooks['astro:config:setup']({ ...baseConfigHookObject, updateConfig, injectScript, config });
expect(updateConfig).toHaveBeenCalledTimes(0);
expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0);
});
+ it("doesn't enable source maps if `sourcemaps.disable` is `true`", async () => {
+ const integration = sentryAstro({
+ sourcemaps: { disable: true },
+ });
+
+ expect(integration.hooks['astro:config:setup']).toBeDefined();
+ // @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(updateConfig).toHaveBeenCalledTimes(0);
+ expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0);
+ });
+
+ it('enables source maps if `sourcemaps.disable` is not defined', async () => {
+ const integration = sentryAstro({});
+
+ expect(integration.hooks['astro:config:setup']).toBeDefined();
+ // @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(updateConfig).toHaveBeenCalledTimes(1);
+ expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1);
+ });
+
it("doesn't add the Vite plugin in dev mode", async () => {
const integration = sentryAstro({
sourceMapsUploadOptions: { enabled: true },
@@ -273,7 +313,13 @@ describe('sentryAstro integration', () => {
expect(integration.hooks['astro:config:setup']).toBeDefined();
// @ts-expect-error - the hook exists and we only need to pass what we actually use
- await integration.hooks['astro:config:setup']({ updateConfig, injectScript, config, command: 'dev' });
+ await integration.hooks['astro:config:setup']({
+ ...baseConfigHookObject,
+ updateConfig,
+ injectScript,
+ config,
+ command: 'dev',
+ });
expect(updateConfig).toHaveBeenCalledTimes(0);
expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0);
diff --git a/packages/astro/test/integration/snippets.test.ts b/packages/astro/test/integration/snippets.test.ts
index edc8338906ab..4c3f1a88d25d 100644
--- a/packages/astro/test/integration/snippets.test.ts
+++ b/packages/astro/test/integration/snippets.test.ts
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from '../../src/integration/snippets';
+import type { SentryOptions } from '../../src/integration/types';
-const allSdkOptions = {
+const allSdkOptions: SentryOptions = {
dsn: 'my-dsn',
release: '1.0.0',
environment: 'staging',
diff --git a/packages/astro/tsconfig.test.json b/packages/astro/tsconfig.test.json
index c41efeacd92f..da5a816712e3 100644
--- a/packages/astro/tsconfig.test.json
+++ b/packages/astro/tsconfig.test.json
@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.json",
- "include": ["test/**/*", "vite.config.ts"],
+ "include": ["test/**/*"],
"compilerOptions": {
// should include all types from `./tsconfig.json` plus types for all test frameworks used
diff --git a/packages/astro/tsconfig.vite.json b/packages/astro/tsconfig.vite.json
new file mode 100644
index 000000000000..a3d6e59b1bfe
--- /dev/null
+++ b/packages/astro/tsconfig.vite.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["vite.config.ts"],
+
+ "compilerOptions": {
+ // should include all types from `./tsconfig.json` plus types for all test frameworks used
+ "types": ["node"]
+ }
+}
diff --git a/packages/astro/vite.config.ts b/packages/astro/vite.config.ts
index f18ec92095bc..5f83f34483c3 100644
--- a/packages/astro/vite.config.ts
+++ b/packages/astro/vite.config.ts
@@ -4,5 +4,9 @@ export default {
...baseConfig,
test: {
...baseConfig.test,
+ typecheck: {
+ enabled: true,
+ tsconfig: './tsconfig.test.json',
+ },
},
};
diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json
index 708a9376ba3a..d24ff2560a05 100644
--- a/packages/aws-serverless/package.json
+++ b/packages/aws-serverless/package.json
@@ -67,8 +67,8 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/instrumentation": "^0.203.0",
- "@opentelemetry/instrumentation-aws-lambda": "0.54.0",
"@opentelemetry/instrumentation-aws-sdk": "0.56.0",
+ "@opentelemetry/semantic-conventions": "^1.36.0",
"@sentry/core": "10.5.0",
"@sentry/node": "10.5.0",
"@types/aws-lambda": "^8.10.62"
diff --git a/packages/aws-serverless/src/awslambda-auto.ts b/packages/aws-serverless/src/awslambda-auto.ts
index 2f23fe652005..5848aa08e568 100644
--- a/packages/aws-serverless/src/awslambda-auto.ts
+++ b/packages/aws-serverless/src/awslambda-auto.ts
@@ -22,10 +22,6 @@ if (lambdaTaskRoot) {
: {},
),
});
-
- if (typeof require !== 'undefined') {
- Sentry.tryPatchHandler(lambdaTaskRoot, handlerString);
- }
} else {
throw Error('LAMBDA_TASK_ROOT environment variable is not set');
}
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index 8cbcd31c50a5..7d7455d496bb 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -122,6 +122,7 @@ export {
zodErrorsIntegration,
profiler,
amqplibIntegration,
+ anthropicAIIntegration,
vercelAIIntegration,
logger,
consoleLoggingIntegration,
@@ -147,5 +148,7 @@ export {
export { awsIntegration } from './integration/aws';
export { awsLambdaIntegration } from './integration/awslambda';
-export { getDefaultIntegrations, init, tryPatchHandler, wrapHandler } from './sdk';
+export { getDefaultIntegrations, init } from './init';
+// eslint-disable-next-line deprecation/deprecation
+export { tryPatchHandler, wrapHandler } from './sdk';
export type { WrapperOptions } from './sdk';
diff --git a/packages/aws-serverless/src/init.ts b/packages/aws-serverless/src/init.ts
new file mode 100644
index 000000000000..269cc3fe27fb
--- /dev/null
+++ b/packages/aws-serverless/src/init.ts
@@ -0,0 +1,31 @@
+import type { Integration, Options } from '@sentry/core';
+import { applySdkMetadata, getSDKSource } from '@sentry/core';
+import type { NodeClient, NodeOptions } from '@sentry/node';
+import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node';
+import { awsIntegration } from './integration/aws';
+import { awsLambdaIntegration } from './integration/awslambda';
+
+/**
+ * Get the default integrations for the AWSLambda SDK.
+ */
+// NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations.
+// If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there.
+export function getDefaultIntegrations(_options: Options): Integration[] {
+ return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()];
+}
+
+/**
+ * Initializes the Sentry AWS Lambda SDK.
+ *
+ * @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}.
+ */
+export function init(options: NodeOptions = {}): NodeClient | undefined {
+ const opts = {
+ defaultIntegrations: getDefaultIntegrations(options),
+ ...options,
+ };
+
+ applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource());
+
+ return initWithoutDefaultIntegrations(opts);
+}
diff --git a/packages/aws-serverless/src/integration/awslambda.ts b/packages/aws-serverless/src/integration/awslambda.ts
index 00bca1a9219c..c459fc8e25e8 100644
--- a/packages/aws-serverless/src/integration/awslambda.ts
+++ b/packages/aws-serverless/src/integration/awslambda.ts
@@ -1,8 +1,8 @@
-import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
-import { generateInstrumentOnce } from '@sentry/node';
-import { eventContextExtractor } from '../utils';
+import { captureException, generateInstrumentOnce } from '@sentry/node';
+import { eventContextExtractor, markEventUnhandled } from '../utils';
+import { AwsLambdaInstrumentation } from './instrumentation-aws-lambda/instrumentation';
interface AwsLambdaOptions {
/**
@@ -27,6 +27,11 @@ export const instrumentAwsLambda = generateInstrumentOnce(
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
+ responseHook(_span, { err }) {
+ if (err) {
+ captureException(err, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.otel'));
+ }
+ },
};
},
);
diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts
new file mode 100644
index 000000000000..39b63551b2aa
--- /dev/null
+++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts
@@ -0,0 +1,526 @@
+// Vendored and modified from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/instrumentation.ts
+// Modifications:
+// - Added Sentry `wrapHandler` around the OTel patch handler.
+// - Cancel init when handler string is invalid (TS)
+// - Hardcoded package version and name
+/* eslint-disable */
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type {
+ Attributes,
+ Context as OtelContext,
+ MeterProvider,
+ Span,
+ TextMapGetter,
+ TracerProvider,
+} from '@opentelemetry/api';
+import {
+ context as otelContext,
+ diag,
+ propagation,
+ ROOT_CONTEXT,
+ SpanKind,
+ SpanStatusCode,
+ trace,
+} from '@opentelemetry/api';
+import {
+ InstrumentationBase,
+ InstrumentationNodeModuleDefinition,
+ InstrumentationNodeModuleFile,
+ isWrapped,
+ safeExecuteInTheMiddle,
+} from '@opentelemetry/instrumentation';
+import {
+ ATTR_URL_FULL,
+ SEMATTRS_FAAS_EXECUTION,
+ SEMRESATTRS_CLOUD_ACCOUNT_ID,
+ SEMRESATTRS_FAAS_ID,
+} from '@opentelemetry/semantic-conventions';
+import type { APIGatewayProxyEventHeaders, Callback, Context, Handler } from 'aws-lambda';
+import * as fs from 'fs';
+import * as path from 'path';
+import type { LambdaModule } from './internal-types';
+import { ATTR_FAAS_COLDSTART } from './semconv';
+import type { AwsLambdaInstrumentationConfig, EventContextExtractor } from './types';
+import { wrapHandler } from '../../sdk';
+import { SDK_VERSION } from '@sentry/core';
+
+// OpenTelemetry package version was 0.54.0 at time of vendoring.
+const PACKAGE_VERSION = SDK_VERSION;
+const PACKAGE_NAME = '@sentry/instrumentation-aws-lambda';
+
+const headerGetter: TextMapGetter = {
+ keys(carrier): string[] {
+ return Object.keys(carrier);
+ },
+ get(carrier, key: string) {
+ return carrier[key];
+ },
+};
+
+export const lambdaMaxInitInMilliseconds = 10_000;
+
+/**
+ *
+ */
+export class AwsLambdaInstrumentation extends InstrumentationBase {
+ private declare _traceForceFlusher?: () => Promise;
+ private declare _metricForceFlusher?: () => Promise;
+
+ constructor(config: AwsLambdaInstrumentationConfig = {}) {
+ super(PACKAGE_NAME, PACKAGE_VERSION, config);
+ }
+
+ /**
+ *
+ */
+ init() {
+ const taskRoot = process.env.LAMBDA_TASK_ROOT;
+ const handlerDef = this.getConfig().lambdaHandler ?? process.env._HANDLER;
+
+ // _HANDLER and LAMBDA_TASK_ROOT are always defined in Lambda but guard bail out if in the future this changes.
+ if (!taskRoot || !handlerDef) {
+ this._diag.debug('Skipping lambda instrumentation: no _HANDLER/lambdaHandler or LAMBDA_TASK_ROOT.', {
+ taskRoot,
+ handlerDef,
+ });
+ return [];
+ }
+
+ const handler = path.basename(handlerDef);
+ const moduleRoot = handlerDef.substring(0, handlerDef.length - handler.length);
+
+ const [module, functionName] = handler.split('.', 2);
+
+ if (!module || !functionName) {
+ this._diag.warn('Invalid handler definition', {
+ handler,
+ moduleRoot,
+ module,
+ });
+ return [];
+ }
+
+ // Lambda loads user function using an absolute path.
+ let filename = path.resolve(taskRoot, moduleRoot, module);
+ if (!filename.endsWith('.js')) {
+ // It's impossible to know in advance if the user has a js, mjs or cjs file.
+ // Check that the .js file exists otherwise fallback to the next known possibilities (.mjs, .cjs).
+ try {
+ fs.statSync(`${filename}.js`);
+ filename += '.js';
+ } catch (e) {
+ try {
+ fs.statSync(`${filename}.mjs`);
+ // fallback to .mjs (ESM)
+ filename += '.mjs';
+ } catch (e2) {
+ try {
+ fs.statSync(`${filename}.cjs`);
+ // fallback to .cjs (CommonJS)
+ filename += '.cjs';
+ } catch (e3) {
+ this._diag.warn(
+ 'No handler file was able to resolved with one of the known extensions for the file',
+ filename,
+ );
+ }
+ }
+ }
+ }
+
+ diag.debug('Instrumenting lambda handler', {
+ taskRoot,
+ handlerDef,
+ handler,
+ moduleRoot,
+ module,
+ filename,
+ functionName,
+ });
+
+ const lambdaStartTime = this.getConfig().lambdaStartTime || Date.now() - Math.floor(1000 * process.uptime());
+
+ return [
+ new InstrumentationNodeModuleDefinition(
+ // NB: The patching infrastructure seems to match names backwards, this must be the filename, while
+ // InstrumentationNodeModuleFile must be the module name.
+ filename,
+ ['*'],
+ undefined,
+ undefined,
+ [
+ new InstrumentationNodeModuleFile(
+ module,
+ ['*'],
+ (moduleExports: LambdaModule) => {
+ if (isWrapped(moduleExports[functionName])) {
+ this._unwrap(moduleExports, functionName);
+ }
+ this._wrap(moduleExports, functionName, this._getHandler(lambdaStartTime));
+ return moduleExports;
+ },
+ (moduleExports?: LambdaModule) => {
+ if (moduleExports == null) return;
+ this._unwrap(moduleExports, functionName);
+ },
+ ),
+ ],
+ ),
+ ];
+ }
+
+ /**
+ *
+ */
+ private _getHandler(handlerLoadStartTime: number) {
+ return (original: Handler) => {
+ return wrapHandler(this._getPatchHandler(original, handlerLoadStartTime));
+ };
+ }
+
+ /**
+ *
+ */
+ private _getPatchHandler(original: Handler, lambdaStartTime: number) {
+ diag.debug('patch handler function');
+ const plugin = this;
+
+ let requestHandledBefore = false;
+ let requestIsColdStart = true;
+
+ /**
+ *
+ */
+ function _onRequest(): void {
+ if (requestHandledBefore) {
+ // Non-first requests cannot be coldstart.
+ requestIsColdStart = false;
+ } else {
+ if (process.env.AWS_LAMBDA_INITIALIZATION_TYPE === 'provisioned-concurrency') {
+ // If sandbox environment is initialized with provisioned concurrency,
+ // even the first requests should not be considered as coldstart.
+ requestIsColdStart = false;
+ } else {
+ // Check whether it is proactive initialization or not:
+ // https://aaronstuyvenberg.com/posts/understanding-proactive-initialization
+ const passedTimeSinceHandlerLoad: number = Date.now() - lambdaStartTime;
+ const proactiveInitialization: boolean = passedTimeSinceHandlerLoad > lambdaMaxInitInMilliseconds;
+
+ // If sandbox has been initialized proactively before the actual request,
+ // even the first requests should not be considered as coldstart.
+ requestIsColdStart = !proactiveInitialization;
+ }
+ requestHandledBefore = true;
+ }
+ }
+
+ return function patchedHandler(
+ this: never,
+ // The event can be a user type, it truly is any.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ event: any,
+ context: Context,
+ callback: Callback,
+ ) {
+ _onRequest();
+
+ const config = plugin.getConfig();
+ const parent = AwsLambdaInstrumentation._determineParent(
+ event,
+ context,
+ config.eventContextExtractor || AwsLambdaInstrumentation._defaultEventContextExtractor,
+ );
+
+ const name = context.functionName;
+ const span = plugin.tracer.startSpan(
+ name,
+ {
+ kind: SpanKind.SERVER,
+ attributes: {
+ [SEMATTRS_FAAS_EXECUTION]: context.awsRequestId,
+ [SEMRESATTRS_FAAS_ID]: context.invokedFunctionArn,
+ [SEMRESATTRS_CLOUD_ACCOUNT_ID]: AwsLambdaInstrumentation._extractAccountId(context.invokedFunctionArn),
+ [ATTR_FAAS_COLDSTART]: requestIsColdStart,
+ ...AwsLambdaInstrumentation._extractOtherEventFields(event),
+ },
+ },
+ parent,
+ );
+
+ const { requestHook } = config;
+ if (requestHook) {
+ safeExecuteInTheMiddle(
+ () => requestHook(span, { event, context }),
+ e => {
+ if (e) diag.error('aws-lambda instrumentation: requestHook error', e);
+ },
+ true,
+ );
+ }
+
+ return otelContext.with(trace.setSpan(parent, span), () => {
+ // Lambda seems to pass a callback even if handler is of Promise form, so we wrap all the time before calling
+ // the handler and see if the result is a Promise or not. In such a case, the callback is usually ignored. If
+ // the handler happened to both call the callback and complete a returned Promise, whichever happens first will
+ // win and the latter will be ignored.
+ const wrappedCallback = plugin._wrapCallback(callback, span);
+ const maybePromise = safeExecuteInTheMiddle(
+ () => original.apply(this, [event, context, wrappedCallback]),
+ error => {
+ if (error != null) {
+ // Exception thrown synchronously before resolving callback / promise.
+ plugin._applyResponseHook(span, error);
+ plugin._endSpan(span, error, () => {});
+ }
+ },
+ ) as Promise<{}> | undefined;
+ if (typeof maybePromise?.then === 'function') {
+ return maybePromise.then(
+ value => {
+ plugin._applyResponseHook(span, null, value);
+ return new Promise(resolve => plugin._endSpan(span, undefined, () => resolve(value)));
+ },
+ (err: Error | string) => {
+ plugin._applyResponseHook(span, err);
+ return new Promise((resolve, reject) => plugin._endSpan(span, err, () => reject(err)));
+ },
+ );
+ }
+ return maybePromise;
+ });
+ };
+ }
+
+ /**
+ *
+ */
+ override setTracerProvider(tracerProvider: TracerProvider) {
+ super.setTracerProvider(tracerProvider);
+ this._traceForceFlusher = this._traceForceFlush(tracerProvider);
+ }
+
+ /**
+ *
+ */
+ private _traceForceFlush(tracerProvider: TracerProvider) {
+ if (!tracerProvider) return undefined;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let currentProvider: any = tracerProvider;
+
+ if (typeof currentProvider.getDelegate === 'function') {
+ currentProvider = currentProvider.getDelegate();
+ }
+
+ if (typeof currentProvider.forceFlush === 'function') {
+ return currentProvider.forceFlush.bind(currentProvider);
+ }
+
+ return undefined;
+ }
+
+ /**
+ *
+ */
+ override setMeterProvider(meterProvider: MeterProvider) {
+ super.setMeterProvider(meterProvider);
+ this._metricForceFlusher = this._metricForceFlush(meterProvider);
+ }
+
+ /**
+ *
+ */
+ private _metricForceFlush(meterProvider: MeterProvider) {
+ if (!meterProvider) return undefined;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const currentProvider: any = meterProvider;
+
+ if (typeof currentProvider.forceFlush === 'function') {
+ return currentProvider.forceFlush.bind(currentProvider);
+ }
+
+ return undefined;
+ }
+
+ /**
+ *
+ */
+ private _wrapCallback(original: Callback, span: Span): Callback {
+ const plugin = this;
+ return function wrappedCallback(this: never, err, res) {
+ diag.debug('executing wrapped lookup callback function');
+ plugin._applyResponseHook(span, err, res);
+
+ plugin._endSpan(span, err, () => {
+ diag.debug('executing original lookup callback function');
+ return original.apply(this, [err, res]);
+ });
+ };
+ }
+
+ /**
+ *
+ */
+ private _endSpan(span: Span, err: string | Error | null | undefined, callback: () => void) {
+ if (err) {
+ span.recordException(err);
+ }
+
+ let errMessage;
+ if (typeof err === 'string') {
+ errMessage = err;
+ } else if (err) {
+ errMessage = err.message;
+ }
+ if (errMessage) {
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: errMessage,
+ });
+ }
+
+ span.end();
+
+ const flushers = [];
+ if (this._traceForceFlusher) {
+ flushers.push(this._traceForceFlusher());
+ } else {
+ diag.debug(
+ 'Spans may not be exported for the lambda function because we are not force flushing before callback.',
+ );
+ }
+ if (this._metricForceFlusher) {
+ flushers.push(this._metricForceFlusher());
+ } else {
+ diag.debug(
+ 'Metrics may not be exported for the lambda function because we are not force flushing before callback.',
+ );
+ }
+
+ Promise.all(flushers).then(callback, callback);
+ }
+
+ /**
+ *
+ */
+ private _applyResponseHook(span: Span, err?: Error | string | null, res?: any) {
+ const { responseHook } = this.getConfig();
+ if (responseHook) {
+ safeExecuteInTheMiddle(
+ () => responseHook(span, { err, res }),
+ e => {
+ if (e) diag.error('aws-lambda instrumentation: responseHook error', e);
+ },
+ true,
+ );
+ }
+ }
+
+ /**
+ *
+ */
+ private static _extractAccountId(arn: string): string | undefined {
+ const parts = arn.split(':');
+ if (parts.length >= 5) {
+ return parts[4];
+ }
+ return undefined;
+ }
+
+ /**
+ *
+ */
+ private static _defaultEventContextExtractor(event: any): OtelContext {
+ // The default extractor tries to get sampled trace header from HTTP headers.
+ const httpHeaders = event.headers || {};
+ return propagation.extract(otelContext.active(), httpHeaders, headerGetter);
+ }
+
+ /**
+ *
+ */
+ private static _extractOtherEventFields(event: any): Attributes {
+ const answer: Attributes = {};
+ const fullUrl = this._extractFullUrl(event);
+ if (fullUrl) {
+ answer[ATTR_URL_FULL] = fullUrl;
+ }
+ return answer;
+ }
+
+ /**
+ *
+ */
+ private static _extractFullUrl(event: any): string | undefined {
+ // API gateway encodes a lot of url information in various places to recompute this
+ if (!event.headers) {
+ return undefined;
+ }
+ // Helper function to deal with case variations (instead of making a tolower() copy of the headers)
+ /**
+ *
+ */
+ function findAny(event: any, key1: string, key2: string): string | undefined {
+ return event.headers[key1] ?? event.headers[key2];
+ }
+ const host = findAny(event, 'host', 'Host');
+ const proto = findAny(event, 'x-forwarded-proto', 'X-Forwarded-Proto');
+ const port = findAny(event, 'x-forwarded-port', 'X-Forwarded-Port');
+ if (!(proto && host && (event.path || event.rawPath))) {
+ return undefined;
+ }
+ let answer = `${proto}://${host}`;
+ if (port) {
+ answer += `:${port}`;
+ }
+ answer += event.path ?? event.rawPath;
+ if (event.queryStringParameters) {
+ let first = true;
+ for (const key in event.queryStringParameters) {
+ answer += first ? '?' : '&';
+ answer += encodeURIComponent(key);
+ answer += '=';
+ answer += encodeURIComponent(event.queryStringParameters[key]);
+ first = false;
+ }
+ }
+ return answer;
+ }
+
+ /**
+ *
+ */
+ private static _determineParent(
+ event: any,
+ context: Context,
+ eventContextExtractor: EventContextExtractor,
+ ): OtelContext {
+ const extractedContext = safeExecuteInTheMiddle(
+ () => eventContextExtractor(event, context),
+ e => {
+ if (e) diag.error('aws-lambda instrumentation: eventContextExtractor error', e);
+ },
+ true,
+ );
+ if (trace.getSpan(extractedContext)?.spanContext()) {
+ return extractedContext;
+ }
+ return ROOT_CONTEXT;
+ }
+}
diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/internal-types.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/internal-types.ts
new file mode 100644
index 000000000000..34894e010fa1
--- /dev/null
+++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/internal-types.ts
@@ -0,0 +1,19 @@
+// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/internal-types.ts
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { Handler } from 'aws-lambda';
+
+export type LambdaModule = Record;
diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/semconv.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/semconv.ts
new file mode 100644
index 000000000000..a10eff490322
--- /dev/null
+++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/semconv.ts
@@ -0,0 +1,29 @@
+// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/semconv.ts
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This file contains a copy of unstable semantic convention definitions
+ * used by this package.
+ * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
+ */
+
+/**
+ * A boolean that is true if the serverless function is executed for the first time (aka cold-start).
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_FAAS_COLDSTART = 'faas.coldstart';
diff --git a/packages/aws-serverless/src/integration/instrumentation-aws-lambda/types.ts b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/types.ts
new file mode 100644
index 000000000000..1b7603281ba0
--- /dev/null
+++ b/packages/aws-serverless/src/integration/instrumentation-aws-lambda/types.ts
@@ -0,0 +1,39 @@
+// Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/cc7eff47e2e7bad7678241b766753d5bd6dbc85f/packages/instrumentation-aws-lambda/src/types.ts
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Context as OtelContext, Span } from '@opentelemetry/api';
+import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
+import type { Context } from 'aws-lambda';
+
+export type RequestHook = (span: Span, hookInfo: { event: any; context: Context }) => void;
+
+export type ResponseHook = (
+ span: Span,
+ hookInfo: {
+ err?: Error | string | null;
+ res?: any;
+ },
+) => void;
+
+export type EventContextExtractor = (event: any, context: Context) => OtelContext;
+export interface AwsLambdaInstrumentationConfig extends InstrumentationConfig {
+ requestHook?: RequestHook;
+ responseHook?: ResponseHook;
+ eventContextExtractor?: EventContextExtractor;
+ lambdaHandler?: string;
+ lambdaStartTime?: number;
+}
diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts
index 9bad62f3a848..e6f7d5f3a4f0 100644
--- a/packages/aws-serverless/src/sdk.ts
+++ b/packages/aws-serverless/src/sdk.ts
@@ -1,23 +1,10 @@
-import type { Integration, Options, Scope } from '@sentry/core';
-import { applySdkMetadata, consoleSandbox, debug, getSDKSource } from '@sentry/core';
-import type { NodeClient, NodeOptions } from '@sentry/node';
-import {
- captureException,
- captureMessage,
- flush,
- getCurrentScope,
- getDefaultIntegrationsWithoutPerformance,
- initWithoutDefaultIntegrations,
- withScope,
-} from '@sentry/node';
+import type { Scope } from '@sentry/core';
+import { consoleSandbox, debug } from '@sentry/core';
+import { captureException, captureMessage, flush, getCurrentScope, withScope } from '@sentry/node';
import type { Context, Handler } from 'aws-lambda';
-import { existsSync } from 'fs';
-import { basename, resolve } from 'path';
import { performance } from 'perf_hooks';
import { types } from 'util';
import { DEBUG_BUILD } from './debug-build';
-import { awsIntegration } from './integration/aws';
-import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
const { isPromise } = types;
@@ -53,42 +40,6 @@ export interface WrapperOptions {
startTrace: boolean;
}
-/**
- * Get the default integrations for the AWSLambda SDK.
- */
-// NOTE: in awslambda-auto.ts, we also call the original `getDefaultIntegrations` from `@sentry/node` to load performance integrations.
-// If at some point we need to filter a node integration out for good, we need to make sure to also filter it out there.
-export function getDefaultIntegrations(_options: Options): Integration[] {
- return [...getDefaultIntegrationsWithoutPerformance(), awsIntegration(), awsLambdaIntegration()];
-}
-
-/**
- * Initializes the Sentry AWS Lambda SDK.
- *
- * @param options Configuration options for the SDK, @see {@link AWSLambdaOptions}.
- */
-export function init(options: NodeOptions = {}): NodeClient | undefined {
- const opts = {
- defaultIntegrations: getDefaultIntegrations(options),
- ...options,
- };
-
- applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource());
-
- return initWithoutDefaultIntegrations(opts);
-}
-
-/** */
-function tryRequire(taskRoot: string, subdir: string, mod: string): T {
- const lambdaStylePath = resolve(taskRoot, subdir, mod);
- if (existsSync(lambdaStylePath) || existsSync(`${lambdaStylePath}.js`)) {
- // Lambda-style path
- return require(lambdaStylePath);
- }
- // Node-style path
- return require(require.resolve(mod, { paths: [taskRoot, subdir] }));
-}
-
/** */
function isPromiseAllSettledResult(result: T[]): boolean {
return result.every(
@@ -108,56 +59,15 @@ function getRejectedReasons(results: PromiseSettledResult[]): T[] {
}, []);
}
-/** */
-export function tryPatchHandler(taskRoot: string, handlerPath: string): void {
- type HandlerBag = HandlerModule | Handler | null | undefined;
-
- interface HandlerModule {
- [key: string]: HandlerBag;
- }
-
- const handlerDesc = basename(handlerPath);
- const match = handlerDesc.match(/^([^.]*)\.(.*)$/);
- if (!match) {
- DEBUG_BUILD && debug.error(`Bad handler ${handlerDesc}`);
- return;
- }
-
- const [, handlerMod = '', handlerName = ''] = match;
-
- let obj: HandlerBag;
- try {
- const handlerDir = handlerPath.substring(0, handlerPath.indexOf(handlerDesc));
- obj = tryRequire(taskRoot, handlerDir, handlerMod);
- } catch (e) {
- DEBUG_BUILD && debug.error(`Cannot require ${handlerPath} in ${taskRoot}`, e);
- return;
- }
-
- let mod: HandlerBag;
- let functionName: string | undefined;
- handlerName.split('.').forEach(name => {
- mod = obj;
- obj = obj && (obj as HandlerModule)[name];
- functionName = name;
+/**
+ * TODO(v11): Remove this function
+ * @deprecated This function is no longer used and will be removed in a future major version.
+ */
+export function tryPatchHandler(_taskRoot: string, _handlerPath: string): void {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn('The `tryPatchHandler` function is deprecated and will be removed in a future major version.');
});
- if (!obj) {
- DEBUG_BUILD && debug.error(`${handlerPath} is undefined or not exported`);
- return;
- }
- if (typeof obj !== 'function') {
- DEBUG_BUILD && debug.error(`${handlerPath} is not a function`);
- return;
- }
-
- // Check for prototype pollution
- if (functionName === '__proto__' || functionName === 'constructor' || functionName === 'prototype') {
- DEBUG_BUILD && debug.error(`Invalid handler name: ${functionName}`);
- return;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (mod as HandlerModule)[functionName!] = wrapHandler(obj);
}
/**
@@ -301,6 +211,8 @@ export function wrapHandler(
});
}
} catch (e) {
+ // Errors should already captured in the instrumentation's `responseHook`,
+ // we capture them here just to be safe. Double captures are deduplicated by the SDK.
captureException(e, scope => markEventUnhandled(scope, 'auto.function.aws-serverless.handler'));
throw e;
} finally {
diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts
index 648ef4caeaec..58bb04a234b9 100644
--- a/packages/aws-serverless/test/sdk.test.ts
+++ b/packages/aws-serverless/test/sdk.test.ts
@@ -1,7 +1,8 @@
import type { Event } from '@sentry/core';
import type { Callback, Handler } from 'aws-lambda';
import { beforeEach, describe, expect, test, vi } from 'vitest';
-import { init, wrapHandler } from '../src/sdk';
+import { init } from '../src/init';
+import { wrapHandler } from '../src/sdk';
const mockFlush = vi.fn((...args) => Promise.resolve(args));
const mockWithScope = vi.fn();
diff --git a/packages/browser/src/integrations/webWorker.ts b/packages/browser/src/integrations/webWorker.ts
index f422f372a463..1f7a266a9c8e 100644
--- a/packages/browser/src/integrations/webWorker.ts
+++ b/packages/browser/src/integrations/webWorker.ts
@@ -25,7 +25,7 @@ interface WebWorkerIntegration extends Integration {
* any messages from the worker. Otherwise, your message handlers will receive
* messages from the Sentry SDK which you need to ignore.
*
- * This integration only has an effect, if you call `Sentry.registerWorker(self)`
+ * This integration only has an effect, if you call `Sentry.registerWebWorker(self)`
* from within the worker(s) you're adding to the integration.
*
* Given that you want to initialize the SDK as early as possible, you most likely
@@ -113,8 +113,21 @@ function listenForSentryDebugIdMessages(worker: Worker): void {
});
}
+/**
+ * Minimal interface for DedicatedWorkerGlobalScope, only requiring the postMessage method.
+ * (which is the only thing we need from the worker's global object)
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope
+ *
+ * We can't use the actual type because it breaks everyone who doesn't have {"lib": ["WebWorker"]}
+ * but uses {"skipLibCheck": true} in their tsconfig.json.
+ */
+interface MinimalDedicatedWorkerGlobalScope {
+ postMessage: (message: unknown) => void;
+}
+
interface RegisterWebWorkerOptions {
- self: Worker & { _sentryDebugIds?: Record };
+ self: MinimalDedicatedWorkerGlobalScope & { _sentryDebugIds?: Record };
}
/**
@@ -125,7 +138,7 @@ interface RegisterWebWorkerOptions {
* import * as Sentry from '@sentry/';
*
* // Do this as early as possible in your worker.
- * Sentry.registerWorker({ self });
+ * Sentry.registerWebWorker({ self });
*
* // continue setting up your worker
* self.postMessage(...)
diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json
index 6b204e508047..1ac927bde013 100644
--- a/packages/browser/tsconfig.json
+++ b/packages/browser/tsconfig.json
@@ -4,6 +4,6 @@
"include": ["src/**/*", "test/loader.js"],
"compilerOptions": {
- "lib": ["DOM", "ES2018"],
+ "lib": ["DOM", "ES2018", "WebWorker"]
}
}
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index b9af910eb0f1..ec092bcdbbba 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -140,6 +140,7 @@ export {
zodErrorsIntegration,
profiler,
amqplibIntegration,
+ anthropicAIIntegration,
vercelAIIntegration,
logger,
consoleLoggingIntegration,
diff --git a/packages/cloudflare/test/integrations/fetch.test.ts b/packages/cloudflare/test/integrations/fetch.test.ts
index 724ff39c7dde..2a8f9cf6e718 100644
--- a/packages/cloudflare/test/integrations/fetch.test.ts
+++ b/packages/cloudflare/test/integrations/fetch.test.ts
@@ -101,8 +101,8 @@ describe('WinterCGFetch instrumentation', () => {
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startHandlerData: HandlerDataFetch = {
- fetchData: { url: 'https://dsn.ingest.sentry.io/1337', method: 'POST' },
- args: ['https://dsn.ingest.sentry.io/1337'],
+ fetchData: { url: 'https://dsn.ingest.sentry.io/1337?sentry_key=123', method: 'POST' },
+ args: ['https://dsn.ingest.sentry.io/1337?sentry_key=123'],
startTimestamp: Date.now(),
};
fetchInstrumentationHandlerCallback(startHandlerData);
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index f81a6937d89c..6385a75687f7 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -121,9 +121,13 @@ export type { ReportDialogOptions } from './report-dialog';
export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports';
export { consoleLoggingIntegration } from './logs/console-integration';
export { addVercelAiProcessors } from './utils/vercel-ai';
+export { _INTERNAL_getSpanForToolCallId, _INTERNAL_cleanupToolCallSpan } from './utils/vercel-ai/utils';
export { instrumentOpenAiClient } from './utils/openai';
export { OPENAI_INTEGRATION_NAME } from './utils/openai/constants';
+export { instrumentAnthropicAiClient } from './utils/anthropic-ai';
+export { ANTHROPIC_AI_INTEGRATION_NAME } from './utils/anthropic-ai/constants';
export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './utils/openai/types';
+export type { AnthropicAiClient, AnthropicAiOptions, AnthropicAiInstrumentedMethod } from './utils/anthropic-ai/types';
export type { FeatureFlag } from './utils/featureFlags';
export {
diff --git a/packages/core/src/utils/gen-ai-attributes.ts b/packages/core/src/utils/ai/gen-ai-attributes.ts
similarity index 91%
rename from packages/core/src/utils/gen-ai-attributes.ts
rename to packages/core/src/utils/ai/gen-ai-attributes.ts
index d1b45532e8a5..9124602644e4 100644
--- a/packages/core/src/utils/gen-ai-attributes.ts
+++ b/packages/core/src/utils/ai/gen-ai-attributes.ts
@@ -8,6 +8,11 @@
// OPENTELEMETRY SEMANTIC CONVENTIONS FOR GENAI
// =============================================================================
+/**
+ * The input messages sent to the model
+ */
+export const GEN_AI_PROMPT_ATTRIBUTE = 'gen_ai.prompt';
+
/**
* The Generative AI system being used
* For OpenAI, this should always be "openai"
@@ -164,3 +169,12 @@ export const OPENAI_OPERATIONS = {
CHAT: 'chat',
RESPONSES: 'responses',
} as const;
+
+// =============================================================================
+// ANTHROPIC AI OPERATIONS
+// =============================================================================
+
+/**
+ * The response timestamp from Anthropic AI (ISO string)
+ */
+export const ANTHROPIC_AI_RESPONSE_TIMESTAMP_ATTRIBUTE = 'anthropic.response.timestamp';
diff --git a/packages/core/src/utils/ai/utils.ts b/packages/core/src/utils/ai/utils.ts
new file mode 100644
index 000000000000..2a2952ce6ad8
--- /dev/null
+++ b/packages/core/src/utils/ai/utils.ts
@@ -0,0 +1,83 @@
+/**
+ * Shared utils for AI integrations (OpenAI, Anthropic, Verce.AI, etc.)
+ */
+import type { Span } from '../../types-hoist/span';
+import {
+ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
+} from './gen-ai-attributes';
+/**
+ * Maps AI method paths to Sentry operation name
+ */
+export function getFinalOperationName(methodPath: string): string {
+ if (methodPath.includes('messages')) {
+ return 'messages';
+ }
+ if (methodPath.includes('completions')) {
+ return 'completions';
+ }
+ if (methodPath.includes('models')) {
+ return 'models';
+ }
+ return methodPath.split('.').pop() || 'unknown';
+}
+
+/**
+ * Get the span operation for AI methods
+ * Following Sentry's convention: "gen_ai.{operation_name}"
+ */
+export function getSpanOperation(methodPath: string): string {
+ return `gen_ai.${getFinalOperationName(methodPath)}`;
+}
+
+/**
+ * Build method path from current traversal
+ */
+export function buildMethodPath(currentPath: string, prop: string): string {
+ return currentPath ? `${currentPath}.${prop}` : prop;
+}
+
+/**
+ * Set token usage attributes
+ * @param span - The span to add attributes to
+ * @param promptTokens - The number of prompt tokens
+ * @param completionTokens - The number of completion tokens
+ * @param cachedInputTokens - The number of cached input tokens
+ * @param cachedOutputTokens - The number of cached output tokens
+ */
+export function setTokenUsageAttributes(
+ span: Span,
+ promptTokens?: number,
+ completionTokens?: number,
+ cachedInputTokens?: number,
+ cachedOutputTokens?: number,
+): void {
+ if (promptTokens !== undefined) {
+ span.setAttributes({
+ [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: promptTokens,
+ });
+ }
+ if (completionTokens !== undefined) {
+ span.setAttributes({
+ [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: completionTokens,
+ });
+ }
+ if (
+ promptTokens !== undefined ||
+ completionTokens !== undefined ||
+ cachedInputTokens !== undefined ||
+ cachedOutputTokens !== undefined
+ ) {
+ /**
+ * Total input tokens in a request is the summation of `input_tokens`,
+ * `cache_creation_input_tokens`, and `cache_read_input_tokens`.
+ */
+ const totalTokens =
+ (promptTokens ?? 0) + (completionTokens ?? 0) + (cachedInputTokens ?? 0) + (cachedOutputTokens ?? 0);
+
+ span.setAttributes({
+ [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: totalTokens,
+ });
+ }
+}
diff --git a/packages/core/src/utils/anthropic-ai/constants.ts b/packages/core/src/utils/anthropic-ai/constants.ts
new file mode 100644
index 000000000000..41a227f171e0
--- /dev/null
+++ b/packages/core/src/utils/anthropic-ai/constants.ts
@@ -0,0 +1,11 @@
+export const ANTHROPIC_AI_INTEGRATION_NAME = 'Anthropic_AI';
+
+// https://docs.anthropic.com/en/api/messages
+// https://docs.anthropic.com/en/api/models-list
+export const ANTHROPIC_AI_INSTRUMENTED_METHODS = [
+ 'messages.create',
+ 'messages.countTokens',
+ 'models.get',
+ 'completions.create',
+ 'models.retrieve',
+] as const;
diff --git a/packages/core/src/utils/anthropic-ai/index.ts b/packages/core/src/utils/anthropic-ai/index.ts
new file mode 100644
index 000000000000..8d56b2a56c04
--- /dev/null
+++ b/packages/core/src/utils/anthropic-ai/index.ts
@@ -0,0 +1,242 @@
+import { getCurrentScope } from '../../currentScopes';
+import { captureException } from '../../exports';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
+import { startSpan } from '../../tracing/trace';
+import type { Span, SpanAttributeValue } from '../../types-hoist/span';
+import {
+ ANTHROPIC_AI_RESPONSE_TIMESTAMP_ATTRIBUTE,
+ GEN_AI_OPERATION_NAME_ATTRIBUTE,
+ GEN_AI_PROMPT_ATTRIBUTE,
+ GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE,
+ GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
+ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE,
+ GEN_AI_REQUEST_MODEL_ATTRIBUTE,
+ GEN_AI_REQUEST_STREAM_ATTRIBUTE,
+ GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE,
+ GEN_AI_REQUEST_TOP_K_ATTRIBUTE,
+ GEN_AI_REQUEST_TOP_P_ATTRIBUTE,
+ GEN_AI_RESPONSE_ID_ATTRIBUTE,
+ GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
+ GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
+ GEN_AI_SYSTEM_ATTRIBUTE,
+} from '../ai/gen-ai-attributes';
+import { buildMethodPath, getFinalOperationName, getSpanOperation, setTokenUsageAttributes } from '../ai/utils';
+import { ANTHROPIC_AI_INTEGRATION_NAME } from './constants';
+import type {
+ AnthropicAiClient,
+ AnthropicAiInstrumentedMethod,
+ AnthropicAiIntegration,
+ AnthropicAiOptions,
+ AnthropicAiResponse,
+} from './types';
+import { shouldInstrument } from './utils';
+/**
+ * Extract request attributes from method arguments
+ */
+function extractRequestAttributes(args: unknown[], methodPath: string): Record {
+ const attributes: Record = {
+ [GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic',
+ [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath),
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic',
+ };
+
+ if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
+ const params = args[0] as Record;
+
+ attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = params.model ?? 'unknown';
+ if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = params.temperature;
+ if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = params.top_p;
+ if ('stream' in params) attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] = params.stream;
+ if ('top_k' in params) attributes[GEN_AI_REQUEST_TOP_K_ATTRIBUTE] = params.top_k;
+ if ('frequency_penalty' in params)
+ attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = params.frequency_penalty;
+ if ('max_tokens' in params) attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE] = params.max_tokens;
+ } else {
+ if (methodPath === 'models.retrieve' || methodPath === 'models.get') {
+ // models.retrieve(model-id) and models.get(model-id)
+ attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = args[0];
+ } else {
+ attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = 'unknown';
+ }
+ }
+
+ return attributes;
+}
+
+/**
+ * Add private request attributes to spans.
+ * This is only recorded if recordInputs is true.
+ */
+function addPrivateRequestAttributes(span: Span, params: Record): void {
+ if ('messages' in params) {
+ span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(params.messages) });
+ }
+ if ('input' in params) {
+ span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(params.input) });
+ }
+ if ('prompt' in params) {
+ span.setAttributes({ [GEN_AI_PROMPT_ATTRIBUTE]: JSON.stringify(params.prompt) });
+ }
+}
+
+/**
+ * Add response attributes to spans
+ */
+function addResponseAttributes(span: Span, response: AnthropicAiResponse, recordOutputs?: boolean): void {
+ if (!response || typeof response !== 'object') return;
+
+ // Private response attributes that are only recorded if recordOutputs is true.
+ if (recordOutputs) {
+ // Messages.create
+ if ('content' in response) {
+ if (Array.isArray(response.content)) {
+ span.setAttributes({
+ [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: response.content
+ .map((item: { text: string | undefined }) => item.text)
+ .filter((text): text is string => text !== undefined)
+ .join(''),
+ });
+ }
+ }
+ // Completions.create
+ if ('completion' in response) {
+ span.setAttributes({ [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: response.completion });
+ }
+ // Models.countTokens
+ if ('input_tokens' in response) {
+ span.setAttributes({ [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: JSON.stringify(response.input_tokens) });
+ }
+ }
+
+ span.setAttributes({
+ [GEN_AI_RESPONSE_ID_ATTRIBUTE]: response.id,
+ });
+ span.setAttributes({
+ [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: response.model,
+ });
+ if ('created' in response && typeof response.created === 'number') {
+ span.setAttributes({
+ [ANTHROPIC_AI_RESPONSE_TIMESTAMP_ATTRIBUTE]: new Date(response.created * 1000).toISOString(),
+ });
+ }
+ if ('created_at' in response && typeof response.created_at === 'number') {
+ span.setAttributes({
+ [ANTHROPIC_AI_RESPONSE_TIMESTAMP_ATTRIBUTE]: new Date(response.created_at * 1000).toISOString(),
+ });
+ }
+
+ if (response.usage) {
+ setTokenUsageAttributes(
+ span,
+ response.usage.input_tokens,
+ response.usage.output_tokens,
+ response.usage.cache_creation_input_tokens,
+ response.usage.cache_read_input_tokens,
+ );
+ }
+}
+
+/**
+ * Get record options from the integration
+ */
+function getRecordingOptionsFromIntegration(): AnthropicAiOptions {
+ const scope = getCurrentScope();
+ const client = scope.getClient();
+ const integration = client?.getIntegrationByName(ANTHROPIC_AI_INTEGRATION_NAME) as AnthropicAiIntegration | undefined;
+ const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false;
+
+ return {
+ recordInputs: integration?.options?.recordInputs ?? shouldRecordInputsAndOutputs,
+ recordOutputs: integration?.options?.recordOutputs ?? shouldRecordInputsAndOutputs,
+ };
+}
+
+/**
+ * Instrument a method with Sentry spans
+ * Following Sentry AI Agents Manual Instrumentation conventions
+ * @see https://docs.sentry.io/platforms/javascript/guides/node/tracing/instrumentation/ai-agents-module/#manual-instrumentation
+ */
+function instrumentMethod(
+ originalMethod: (...args: T) => Promise,
+ methodPath: AnthropicAiInstrumentedMethod,
+ context: unknown,
+ options?: AnthropicAiOptions,
+): (...args: T) => Promise {
+ return async function instrumentedMethod(...args: T): Promise {
+ const finalOptions = options || getRecordingOptionsFromIntegration();
+ const requestAttributes = extractRequestAttributes(args, methodPath);
+ const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
+ const operationName = getFinalOperationName(methodPath);
+
+ // TODO: Handle streaming responses
+ return startSpan(
+ {
+ name: `${operationName} ${model}`,
+ op: getSpanOperation(methodPath),
+ attributes: requestAttributes as Record,
+ },
+ async (span: Span) => {
+ try {
+ if (finalOptions.recordInputs && args[0] && typeof args[0] === 'object') {
+ addPrivateRequestAttributes(span, args[0] as Record);
+ }
+
+ const result = await originalMethod.apply(context, args);
+ addResponseAttributes(span, result, finalOptions.recordOutputs);
+ return result;
+ } catch (error) {
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.ai.anthropic',
+ data: {
+ function: methodPath,
+ },
+ },
+ });
+ throw error;
+ }
+ },
+ );
+ };
+}
+
+/**
+ * Create a deep proxy for Anthropic AI client instrumentation
+ */
+function createDeepProxy(target: T, currentPath = '', options?: AnthropicAiOptions): T {
+ return new Proxy(target, {
+ get(obj: object, prop: string): unknown {
+ const value = (obj as Record)[prop];
+ const methodPath = buildMethodPath(currentPath, String(prop));
+
+ if (typeof value === 'function' && shouldInstrument(methodPath)) {
+ return instrumentMethod(value as (...args: unknown[]) => Promise, methodPath, obj, options);
+ }
+
+ if (typeof value === 'function') {
+ // Bind non-instrumented functions to preserve the original `this` context,
+ return value.bind(obj);
+ }
+
+ if (value && typeof value === 'object') {
+ return createDeepProxy(value as object, methodPath, options);
+ }
+
+ return value;
+ },
+ }) as T;
+}
+
+/**
+ * Instrument an Anthropic AI client with Sentry tracing
+ * Can be used across Node.js, Cloudflare Workers, and Vercel Edge
+ *
+ * @template T - The type of the client that extends AnthropicAiClient
+ * @param client - The Anthropic AI client to instrument
+ * @param options - Optional configuration for recording inputs and outputs
+ * @returns The instrumented client with the same type as the input
+ */
+export function instrumentAnthropicAiClient(client: T, options?: AnthropicAiOptions): T {
+ return createDeepProxy(client, '', options);
+}
diff --git a/packages/core/src/utils/anthropic-ai/types.ts b/packages/core/src/utils/anthropic-ai/types.ts
new file mode 100644
index 000000000000..566e9588d56f
--- /dev/null
+++ b/packages/core/src/utils/anthropic-ai/types.ts
@@ -0,0 +1,63 @@
+import type { ANTHROPIC_AI_INSTRUMENTED_METHODS } from './constants';
+
+export interface AnthropicAiOptions {
+ /**
+ * Enable or disable input recording.
+ */
+ recordInputs?: boolean;
+ /**
+ * Enable or disable output recording.
+ */
+ recordOutputs?: boolean;
+}
+
+export type Message = {
+ role: 'user' | 'assistant';
+ content: string | unknown[];
+};
+
+export type AnthropicAiResponse = {
+ [key: string]: unknown; // Allow for additional unknown properties
+ id: string;
+ model: string;
+ created?: number;
+ created_at?: number; // Available for Models.retrieve
+ messages?: Array;
+ content?: string; // Available for Messages.create
+ completion?: string; // Available for Completions.create
+ input_tokens?: number; // Available for Models.countTokens
+ usage?: {
+ input_tokens: number;
+ output_tokens: number;
+ cache_creation_input_tokens: number;
+ cache_read_input_tokens: number;
+ };
+};
+
+/**
+ * Basic interface for Anthropic AI client with only the instrumented methods
+ * This provides type safety while being generic enough to work with different client implementations
+ */
+export interface AnthropicAiClient {
+ messages?: {
+ create: (...args: unknown[]) => Promise;
+ countTokens: (...args: unknown[]) => Promise;
+ };
+ models?: {
+ list: (...args: unknown[]) => Promise;
+ get: (...args: unknown[]) => Promise;
+ };
+ completions?: {
+ create: (...args: unknown[]) => Promise;
+ };
+}
+
+/**
+ * Anthropic AI Integration interface for type safety
+ */
+export interface AnthropicAiIntegration {
+ name: string;
+ options: AnthropicAiOptions;
+}
+
+export type AnthropicAiInstrumentedMethod = (typeof ANTHROPIC_AI_INSTRUMENTED_METHODS)[number];
diff --git a/packages/core/src/utils/anthropic-ai/utils.ts b/packages/core/src/utils/anthropic-ai/utils.ts
new file mode 100644
index 000000000000..299d20170d6c
--- /dev/null
+++ b/packages/core/src/utils/anthropic-ai/utils.ts
@@ -0,0 +1,9 @@
+import { ANTHROPIC_AI_INSTRUMENTED_METHODS } from './constants';
+import type { AnthropicAiInstrumentedMethod } from './types';
+
+/**
+ * Check if a method path should be instrumented
+ */
+export function shouldInstrument(methodPath: string): methodPath is AnthropicAiInstrumentedMethod {
+ return ANTHROPIC_AI_INSTRUMENTED_METHODS.includes(methodPath as AnthropicAiInstrumentedMethod);
+}
diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts
index e93f61a5919a..8cda9404164a 100644
--- a/packages/core/src/utils/isSentryRequestUrl.ts
+++ b/packages/core/src/utils/isSentryRequestUrl.ts
@@ -1,5 +1,6 @@
import type { Client } from '../client';
import type { DsnComponents } from '../types-hoist/dsn';
+import { isURLObjectRelative, parseStringToURLObject } from './url';
/**
* Checks whether given url points to Sentry server
@@ -21,7 +22,17 @@ function checkTunnel(url: string, tunnel: string | undefined): boolean {
}
function checkDsn(url: string, dsn: DsnComponents | undefined): boolean {
- return dsn ? url.includes(dsn.host) : false;
+ // Requests to Sentry's ingest endpoint must have a `sentry_key` in the query string
+ // This is equivalent to the public_key which is required in the DSN
+ // see https://develop.sentry.dev/sdk/overview/#parsing-the-dsn
+ // Therefore, a request to the same host and with a `sentry_key` in the query string
+ // can be considered a request to the ingest endpoint.
+ const urlParts = parseStringToURLObject(url);
+ if (!urlParts || isURLObjectRelative(urlParts)) {
+ return false;
+ }
+
+ return dsn ? urlParts.host.includes(dsn.host) && /(^|&|\?)sentry_key=/.test(urlParts.search) : false;
}
function removeTrailingSlash(str: string): string {
diff --git a/packages/core/src/utils/openai/index.ts b/packages/core/src/utils/openai/index.ts
index 3fb4f0d16fce..3fb8b1bf8b98 100644
--- a/packages/core/src/utils/openai/index.ts
+++ b/packages/core/src/utils/openai/index.ts
@@ -17,7 +17,7 @@ import {
GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
GEN_AI_SYSTEM_ATTRIBUTE,
-} from '../gen-ai-attributes';
+} from '../ai/gen-ai-attributes';
import { OPENAI_INTEGRATION_NAME } from './constants';
import { instrumentStream } from './streaming';
import type {
diff --git a/packages/core/src/utils/openai/streaming.ts b/packages/core/src/utils/openai/streaming.ts
index 2791e715920e..c79448effb35 100644
--- a/packages/core/src/utils/openai/streaming.ts
+++ b/packages/core/src/utils/openai/streaming.ts
@@ -6,7 +6,7 @@ import {
GEN_AI_RESPONSE_STREAMING_ATTRIBUTE,
GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
-} from '../gen-ai-attributes';
+} from '../ai/gen-ai-attributes';
import { RESPONSE_EVENT_TYPES } from './constants';
import type { OpenAIResponseObject } from './types';
import {
diff --git a/packages/core/src/utils/openai/utils.ts b/packages/core/src/utils/openai/utils.ts
index f76d26de5d6a..17007693e739 100644
--- a/packages/core/src/utils/openai/utils.ts
+++ b/packages/core/src/utils/openai/utils.ts
@@ -11,7 +11,7 @@ import {
OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE,
OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
-} from '../gen-ai-attributes';
+} from '../ai/gen-ai-attributes';
import { INSTRUMENTED_METHODS } from './constants';
import type {
ChatCompletionChunk,
diff --git a/packages/core/src/utils/vercel-ai/constants.ts b/packages/core/src/utils/vercel-ai/constants.ts
new file mode 100644
index 000000000000..fe307b03e7fb
--- /dev/null
+++ b/packages/core/src/utils/vercel-ai/constants.ts
@@ -0,0 +1,5 @@
+import type { Span } from '../../types-hoist/span';
+
+// Global Map to track tool call IDs to their corresponding spans
+// This allows us to capture tool errors and link them to the correct span
+export const toolCallSpanMap = new Map();
diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai/index.ts
similarity index 85%
rename from packages/core/src/utils/vercel-ai.ts
rename to packages/core/src/utils/vercel-ai/index.ts
index 4ef437a1b922..4b317fe653d6 100644
--- a/packages/core/src/utils/vercel-ai.ts
+++ b/packages/core/src/utils/vercel-ai/index.ts
@@ -1,8 +1,11 @@
-import type { Client } from '../client';
-import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
-import type { Event } from '../types-hoist/event';
-import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, SpanOrigin } from '../types-hoist/span';
-import { spanToJSON } from './spanUtils';
+import type { Client } from '../../client';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
+import type { Event } from '../../types-hoist/event';
+import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, SpanOrigin } from '../../types-hoist/span';
+import { spanToJSON } from '../spanUtils';
+import { toolCallSpanMap } from './constants';
+import type { TokenSummary } from './types';
+import { accumulateTokensForParent, applyAccumulatedTokens } from './utils';
import type { ProviderMetadata } from './vercel-ai-attributes';
import {
AI_MODEL_ID_ATTRIBUTE,
@@ -60,11 +63,6 @@ function onVercelAiSpanStart(span: Span): void {
processGenerateSpan(span, name, attributes);
}
-interface TokenSummary {
- inputTokens: number;
- outputTokens: number;
-}
-
function vercelAiEventProcessor(event: Event): Event {
if (event.type === 'transaction' && event.spans) {
// Map to accumulate token data by parent span ID
@@ -86,6 +84,12 @@ function vercelAiEventProcessor(event: Event): Event {
applyAccumulatedTokens(span, tokenAccumulator);
}
+
+ // Also apply to root when it is the invoke_agent pipeline
+ const trace = event.contexts?.trace;
+ if (trace && trace.op === 'gen_ai.invoke_agent') {
+ applyAccumulatedTokens(trace, tokenAccumulator);
+ }
}
return event;
@@ -148,6 +152,15 @@ function processToolCallSpan(span: Span, attributes: SpanAttributes): void {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.execute_tool');
renameAttributeKey(attributes, AI_TOOL_CALL_NAME_ATTRIBUTE, 'gen_ai.tool.name');
renameAttributeKey(attributes, AI_TOOL_CALL_ID_ATTRIBUTE, 'gen_ai.tool.call.id');
+
+ // Store the span in our global map using the tool call ID
+ // This allows us to capture tool errors and link them to the correct span
+ const toolCallId = attributes['gen_ai.tool.call.id'];
+
+ if (typeof toolCallId === 'string') {
+ toolCallSpanMap.set(toolCallId, span);
+ }
+
// https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-tool-type
if (!attributes['gen_ai.tool.type']) {
span.setAttribute('gen_ai.tool.type', 'function');
@@ -262,56 +275,6 @@ export function addVercelAiProcessors(client: Client): void {
client.addEventProcessor(Object.assign(vercelAiEventProcessor, { id: 'VercelAiEventProcessor' }));
}
-/**
- * Accumulates token data from a span to its parent in the token accumulator map.
- * This function extracts token usage from the current span and adds it to the
- * accumulated totals for its parent span.
- */
-function accumulateTokensForParent(span: SpanJSON, tokenAccumulator: Map): void {
- const parentSpanId = span.parent_span_id;
- if (!parentSpanId) {
- return;
- }
-
- const inputTokens = span.data[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE];
- const outputTokens = span.data[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE];
-
- if (typeof inputTokens === 'number' || typeof outputTokens === 'number') {
- const existing = tokenAccumulator.get(parentSpanId) || { inputTokens: 0, outputTokens: 0 };
-
- if (typeof inputTokens === 'number') {
- existing.inputTokens += inputTokens;
- }
- if (typeof outputTokens === 'number') {
- existing.outputTokens += outputTokens;
- }
-
- tokenAccumulator.set(parentSpanId, existing);
- }
-}
-
-/**
- * Applies accumulated token data to the `gen_ai.invoke_agent` span.
- * Only immediate children of the `gen_ai.invoke_agent` span are considered,
- * since aggregation will automatically occur for each parent span.
- */
-function applyAccumulatedTokens(span: SpanJSON, tokenAccumulator: Map): void {
- const accumulated = tokenAccumulator.get(span.span_id);
- if (!accumulated) {
- return;
- }
-
- if (accumulated.inputTokens > 0) {
- span.data[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = accumulated.inputTokens;
- }
- if (accumulated.outputTokens > 0) {
- span.data[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = accumulated.outputTokens;
- }
- if (accumulated.inputTokens > 0 || accumulated.outputTokens > 0) {
- span.data['gen_ai.usage.total_tokens'] = accumulated.inputTokens + accumulated.outputTokens;
- }
-}
-
function addProviderMetadataToAttributes(attributes: SpanAttributes): void {
const providerMetadata = attributes[AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE] as string | undefined;
if (providerMetadata) {
diff --git a/packages/core/src/utils/vercel-ai/types.ts b/packages/core/src/utils/vercel-ai/types.ts
new file mode 100644
index 000000000000..03f22c415001
--- /dev/null
+++ b/packages/core/src/utils/vercel-ai/types.ts
@@ -0,0 +1,4 @@
+export interface TokenSummary {
+ inputTokens: number;
+ outputTokens: number;
+}
diff --git a/packages/core/src/utils/vercel-ai/utils.ts b/packages/core/src/utils/vercel-ai/utils.ts
new file mode 100644
index 000000000000..e9df1a4a7f96
--- /dev/null
+++ b/packages/core/src/utils/vercel-ai/utils.ts
@@ -0,0 +1,72 @@
+import type { TraceContext } from '../../types-hoist/context';
+import type { Span, SpanJSON } from '../../types-hoist/span';
+import { GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE } from '../ai/gen-ai-attributes';
+import { toolCallSpanMap } from './constants';
+import type { TokenSummary } from './types';
+
+/**
+ * Accumulates token data from a span to its parent in the token accumulator map.
+ * This function extracts token usage from the current span and adds it to the
+ * accumulated totals for its parent span.
+ */
+export function accumulateTokensForParent(span: SpanJSON, tokenAccumulator: Map): void {
+ const parentSpanId = span.parent_span_id;
+ if (!parentSpanId) {
+ return;
+ }
+
+ const inputTokens = span.data[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE];
+ const outputTokens = span.data[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE];
+
+ if (typeof inputTokens === 'number' || typeof outputTokens === 'number') {
+ const existing = tokenAccumulator.get(parentSpanId) || { inputTokens: 0, outputTokens: 0 };
+
+ if (typeof inputTokens === 'number') {
+ existing.inputTokens += inputTokens;
+ }
+ if (typeof outputTokens === 'number') {
+ existing.outputTokens += outputTokens;
+ }
+
+ tokenAccumulator.set(parentSpanId, existing);
+ }
+}
+
+/**
+ * Applies accumulated token data to the `gen_ai.invoke_agent` span.
+ * Only immediate children of the `gen_ai.invoke_agent` span are considered,
+ * since aggregation will automatically occur for each parent span.
+ */
+export function applyAccumulatedTokens(
+ spanOrTrace: SpanJSON | TraceContext,
+ tokenAccumulator: Map,
+): void {
+ const accumulated = tokenAccumulator.get(spanOrTrace.span_id);
+ if (!accumulated || !spanOrTrace.data) {
+ return;
+ }
+
+ if (accumulated.inputTokens > 0) {
+ spanOrTrace.data[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = accumulated.inputTokens;
+ }
+ if (accumulated.outputTokens > 0) {
+ spanOrTrace.data[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = accumulated.outputTokens;
+ }
+ if (accumulated.inputTokens > 0 || accumulated.outputTokens > 0) {
+ spanOrTrace.data['gen_ai.usage.total_tokens'] = accumulated.inputTokens + accumulated.outputTokens;
+ }
+}
+
+/**
+ * Get the span associated with a tool call ID
+ */
+export function _INTERNAL_getSpanForToolCallId(toolCallId: string): Span | undefined {
+ return toolCallSpanMap.get(toolCallId);
+}
+
+/**
+ * Clean up the span mapping for a tool call ID
+ */
+export function _INTERNAL_cleanupToolCallSpan(toolCallId: string): void {
+ toolCallSpanMap.delete(toolCallId);
+}
diff --git a/packages/core/src/utils/vercel-ai-attributes.ts b/packages/core/src/utils/vercel-ai/vercel-ai-attributes.ts
similarity index 100%
rename from packages/core/src/utils/vercel-ai-attributes.ts
rename to packages/core/src/utils/vercel-ai/vercel-ai-attributes.ts
diff --git a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts
index 195e93493e98..806165fb52be 100644
--- a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts
+++ b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts
@@ -4,20 +4,46 @@ import type { Client } from '../../../src/client';
describe('isSentryRequestUrl', () => {
it.each([
- ['', 'sentry-dsn.com', '', false],
- ['http://sentry-dsn.com/my-url', 'sentry-dsn.com', '', true],
- ['http://sentry-dsn.com', 'sentry-dsn.com', '', true],
- ['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200', true],
- ['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200/', true],
- ['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200', true],
- ['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200', false],
- ])('works with url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string, expected: boolean) => {
+ ['http://sentry-dsn.com/my-url?sentry_key=123', 'sentry-dsn.com', ''],
+
+ ['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200'],
+ ['http://tunnel:4200', 'sentry-dsn.com', 'http://tunnel:4200/'],
+ ['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200'],
+ ['http://tunnel:4200/', 'another-dsn.com', 'http://tunnel:4200'],
+ ])('returns `true` for url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string) => {
+ const client = {
+ getOptions: () => ({ tunnel }),
+ getDsn: () => ({ host: dsn }),
+ } as unknown as Client;
+
+ expect(isSentryRequestUrl(url, client)).toBe(true);
+ });
+
+ it.each([
+ ['http://tunnel:4200/?sentry_key=123', 'another-dsn.com', ''],
+ ['http://sentry-dsn.com/my-url', 'sentry-dsn.com', ''],
+ ['http://sentry-dsn.com', 'sentry-dsn.com', ''],
+ ['http://sAntry-dsn.com/?sentry_key=123', 'sentry-dsn.com', ''],
+ ['http://sAntry-dsn.com/?sAntry_key=123', 'sAntry-dsn.com', ''],
+ ['/ingest', 'sentry-dsn.com', ''],
+ ['/ingest?sentry_key=123', 'sentry-dsn.com', ''],
+ ['/ingest', '', ''],
+ ['', '', ''],
+ ['', 'sentry-dsn.com', ''],
+
+ ['http://tunnel:4200/', 'another-dsn.com', 'http://tunnel:4200/sentry-tunnel'],
+ ['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200'],
+ ['http://tunnel:4200/a', '', 'http://tunnel:4200/'],
+ ])('returns `false` for url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string) => {
const client = {
getOptions: () => ({ tunnel }),
getDsn: () => ({ host: dsn }),
} as unknown as Client;
- // Works with client passed
- expect(isSentryRequestUrl(url, client)).toBe(expected);
+ expect(isSentryRequestUrl(url, client)).toBe(false);
+ });
+
+ it('handles undefined client', () => {
+ expect(isSentryRequestUrl('http://sentry-dsn.com/my-url?sentry_key=123', undefined)).toBe(false);
});
});
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 26ed56f031d8..0b76f7776772 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -120,6 +120,7 @@ export {
zodErrorsIntegration,
profiler,
amqplibIntegration,
+ anthropicAIIntegration,
childProcessIntegration,
createSentryWinstonTransport,
vercelAIIntegration,
diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts
index 53086b7da302..75dc1f845693 100644
--- a/packages/nestjs/src/integrations/nest.ts
+++ b/packages/nestjs/src/integrations/nest.ts
@@ -6,15 +6,15 @@ import { SentryNestInstrumentation } from './sentry-nest-instrumentation';
const INTEGRATION_NAME = 'Nest';
-const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => {
+const instrumentNestCore = generateInstrumentOnce(`${INTEGRATION_NAME}.Core`, () => {
return new NestInstrumentationCore();
});
-const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => {
+const instrumentNestCommon = generateInstrumentOnce(`${INTEGRATION_NAME}.Common`, () => {
return new SentryNestInstrumentation();
});
-const instrumentNestEvent = generateInstrumentOnce('Nest-Event', () => {
+const instrumentNestEvent = generateInstrumentOnce(`${INTEGRATION_NAME}.Event`, () => {
return new SentryNestEventInstrumentation();
});
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index da97071bdd32..f5f3865feffa 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -24,6 +24,7 @@ export { dataloaderIntegration } from './integrations/tracing/dataloader';
export { amqplibIntegration } from './integrations/tracing/amqplib';
export { vercelAIIntegration } from './integrations/tracing/vercelai';
export { openAIIntegration } from './integrations/tracing/openai';
+export { anthropicAIIntegration } from './integrations/tracing/anthropic-ai';
export {
launchDarklyIntegration,
buildLaunchDarklyFlagUsedHandler,
diff --git a/packages/node/src/integrations/tracing/anthropic-ai/index.ts b/packages/node/src/integrations/tracing/anthropic-ai/index.ts
new file mode 100644
index 000000000000..b9ec00013f49
--- /dev/null
+++ b/packages/node/src/integrations/tracing/anthropic-ai/index.ts
@@ -0,0 +1,74 @@
+import type { AnthropicAiOptions, IntegrationFn } from '@sentry/core';
+import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration } from '@sentry/core';
+import { generateInstrumentOnce } from '@sentry/node-core';
+import { SentryAnthropicAiInstrumentation } from './instrumentation';
+
+export const instrumentAnthropicAi = generateInstrumentOnce(
+ ANTHROPIC_AI_INTEGRATION_NAME,
+ () => new SentryAnthropicAiInstrumentation({}),
+);
+
+const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => {
+ return {
+ name: ANTHROPIC_AI_INTEGRATION_NAME,
+ options,
+ setupOnce() {
+ instrumentAnthropicAi();
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Adds Sentry tracing instrumentation for the Anthropic AI SDK.
+ *
+ * This integration is enabled by default.
+ *
+ * When configured, this integration automatically instruments Anthropic AI SDK client instances
+ * to capture telemetry data following OpenTelemetry Semantic Conventions for Generative AI.
+ *
+ * @example
+ * ```javascript
+ * import * as Sentry from '@sentry/node';
+ *
+ * Sentry.init({
+ * integrations: [Sentry.anthropicAIIntegration()],
+ * });
+ * ```
+ *
+ * ## Options
+ *
+ * - `recordInputs`: Whether to record prompt messages (default: respects `sendDefaultPii` client option)
+ * - `recordOutputs`: Whether to record response text (default: respects `sendDefaultPii` client option)
+ *
+ * ### Default Behavior
+ *
+ * By default, the integration will:
+ * - Record inputs and outputs ONLY if `sendDefaultPii` is set to `true` in your Sentry client options
+ * - Otherwise, inputs and outputs are NOT recorded unless explicitly enabled
+ *
+ * @example
+ * ```javascript
+ * // Record inputs and outputs when sendDefaultPii is false
+ * Sentry.init({
+ * integrations: [
+ * Sentry.anthropicAIIntegration({
+ * recordInputs: true,
+ * recordOutputs: true
+ * })
+ * ],
+ * });
+ *
+ * // Never record inputs/outputs regardless of sendDefaultPii
+ * Sentry.init({
+ * sendDefaultPii: true,
+ * integrations: [
+ * Sentry.anthropicAIIntegration({
+ * recordInputs: false,
+ * recordOutputs: false
+ * })
+ * ],
+ * });
+ * ```
+ *
+ */
+export const anthropicAIIntegration = defineIntegration(_anthropicAIIntegration);
diff --git a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts
new file mode 100644
index 000000000000..99fd2c546dd2
--- /dev/null
+++ b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts
@@ -0,0 +1,122 @@
+import {
+ type InstrumentationConfig,
+ type InstrumentationModuleDefinition,
+ InstrumentationBase,
+ InstrumentationNodeModuleDefinition,
+} from '@opentelemetry/instrumentation';
+import type { AnthropicAiClient, AnthropicAiOptions, Integration } from '@sentry/core';
+import { ANTHROPIC_AI_INTEGRATION_NAME, getCurrentScope, instrumentAnthropicAiClient, SDK_VERSION } from '@sentry/core';
+
+const supportedVersions = ['>=0.19.2 <1.0.0'];
+
+export interface AnthropicAiIntegration extends Integration {
+ options: AnthropicAiOptions;
+}
+
+/**
+ * Represents the patched shape of the Anthropic AI module export.
+ */
+interface PatchedModuleExports {
+ [key: string]: unknown;
+ Anthropic: abstract new (...args: unknown[]) => AnthropicAiClient;
+}
+
+/**
+ * Determines telemetry recording settings.
+ */
+function determineRecordingSettings(
+ integrationOptions: AnthropicAiOptions | undefined,
+ defaultEnabled: boolean,
+): { recordInputs: boolean; recordOutputs: boolean } {
+ const recordInputs = integrationOptions?.recordInputs ?? defaultEnabled;
+ const recordOutputs = integrationOptions?.recordOutputs ?? defaultEnabled;
+ return { recordInputs, recordOutputs };
+}
+
+/**
+ * Sentry Anthropic AI instrumentation using OpenTelemetry.
+ */
+export class SentryAnthropicAiInstrumentation extends InstrumentationBase {
+ public constructor(config: InstrumentationConfig = {}) {
+ super('@sentry/instrumentation-anthropic-ai', SDK_VERSION, config);
+ }
+
+ /**
+ * Initializes the instrumentation by defining the modules to be patched.
+ */
+ public init(): InstrumentationModuleDefinition {
+ const module = new InstrumentationNodeModuleDefinition(
+ '@anthropic-ai/sdk',
+ supportedVersions,
+ this._patch.bind(this),
+ );
+ return module;
+ }
+
+ /**
+ * Core patch logic applying instrumentation to the Anthropic AI client constructor.
+ */
+ private _patch(exports: PatchedModuleExports): PatchedModuleExports | void {
+ const Original = exports.Anthropic;
+
+ const WrappedAnthropic = function (this: unknown, ...args: unknown[]) {
+ const instance = Reflect.construct(Original, args);
+ const scopeClient = getCurrentScope().getClient();
+ const integration = scopeClient?.getIntegrationByName(ANTHROPIC_AI_INTEGRATION_NAME);
+ const integrationOpts = integration?.options;
+ const defaultPii = Boolean(scopeClient?.getOptions().sendDefaultPii);
+
+ const { recordInputs, recordOutputs } = determineRecordingSettings(integrationOpts, defaultPii);
+
+ return instrumentAnthropicAiClient(instance as AnthropicAiClient, {
+ recordInputs,
+ recordOutputs,
+ });
+ } as unknown as abstract new (...args: unknown[]) => AnthropicAiClient;
+
+ // Preserve static and prototype chains
+ Object.setPrototypeOf(WrappedAnthropic, Original);
+ Object.setPrototypeOf(WrappedAnthropic.prototype, Original.prototype);
+
+ for (const key of Object.getOwnPropertyNames(Original)) {
+ if (!['length', 'name', 'prototype'].includes(key)) {
+ const descriptor = Object.getOwnPropertyDescriptor(Original, key);
+ if (descriptor) {
+ Object.defineProperty(WrappedAnthropic, key, descriptor);
+ }
+ }
+ }
+
+ // Constructor replacement - handle read-only properties
+ // The Anthropic property might have only a getter, so use defineProperty
+ try {
+ exports.Anthropic = WrappedAnthropic;
+ } catch (error) {
+ // If direct assignment fails, override the property descriptor
+ Object.defineProperty(exports, 'Anthropic', {
+ value: WrappedAnthropic,
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ });
+ }
+
+ // Wrap the default export if it points to the original constructor
+ // Constructor replacement - handle read-only properties
+ // The Anthropic property might have only a getter, so use defineProperty
+ if (exports.default === Original) {
+ try {
+ exports.default = WrappedAnthropic;
+ } catch (error) {
+ // If direct assignment fails, override the property descriptor
+ Object.defineProperty(exports, 'default', {
+ value: WrappedAnthropic,
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ }
+ return exports;
+ }
+}
diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts
index fd8894e29a96..65d783eb8be7 100644
--- a/packages/node/src/integrations/tracing/fastify/index.ts
+++ b/packages/node/src/integrations/tracing/fastify/index.ts
@@ -90,10 +90,11 @@ interface FastifyHandlerOptions {
}
const INTEGRATION_NAME = 'Fastify';
-const INTEGRATION_NAME_V5 = 'Fastify-V5';
-const INTEGRATION_NAME_V3 = 'Fastify-V3';
-export const instrumentFastifyV3 = generateInstrumentOnce(INTEGRATION_NAME_V3, () => new FastifyInstrumentationV3());
+export const instrumentFastifyV3 = generateInstrumentOnce(
+ `${INTEGRATION_NAME}.v3`,
+ () => new FastifyInstrumentationV3(),
+);
function getFastifyIntegration(): ReturnType | undefined {
const client = getClient();
@@ -135,7 +136,7 @@ function handleFastifyError(
}
}
-export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME_V5, () => {
+export const instrumentFastify = generateInstrumentOnce(`${INTEGRATION_NAME}.v5`, () => {
const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation();
const plugin = fastifyOtelInstrumentationInstance.plugin();
diff --git a/packages/node/src/integrations/tracing/hono/index.ts b/packages/node/src/integrations/tracing/hono/index.ts
new file mode 100644
index 000000000000..8876d26b829e
--- /dev/null
+++ b/packages/node/src/integrations/tracing/hono/index.ts
@@ -0,0 +1,35 @@
+import type { IntegrationFn } from '@sentry/core';
+import { defineIntegration } from '@sentry/core';
+import { generateInstrumentOnce } from '@sentry/node-core';
+import { HonoInstrumentation } from './instrumentation';
+
+const INTEGRATION_NAME = 'Hono';
+
+export const instrumentHono = generateInstrumentOnce(INTEGRATION_NAME, () => new HonoInstrumentation());
+
+const _honoIntegration = (() => {
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ instrumentHono();
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Adds Sentry tracing instrumentation for [Hono](https://hono.dev/).
+ *
+ * If you also want to capture errors, you need to call `setupHonoErrorHandler(app)` after you set up your Hono server.
+ *
+ * For more information, see the [hono documentation](https://docs.sentry.io/platforms/javascript/guides/hono/).
+ *
+ * @example
+ * ```javascript
+ * const Sentry = require('@sentry/node');
+ *
+ * Sentry.init({
+ * integrations: [Sentry.honoIntegration()],
+ * })
+ * ```
+ */
+export const honoIntegration = defineIntegration(_honoIntegration);
diff --git a/packages/node/src/integrations/tracing/hono/instrumentation.ts b/packages/node/src/integrations/tracing/hono/instrumentation.ts
new file mode 100644
index 000000000000..81e062560051
--- /dev/null
+++ b/packages/node/src/integrations/tracing/hono/instrumentation.ts
@@ -0,0 +1,84 @@
+import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
+import type { HandlerInterface, Hono, HonoInstance, MiddlewareHandlerInterface, OnHandlerInterface } from './types';
+
+const PACKAGE_NAME = '@sentry/instrumentation-hono';
+const PACKAGE_VERSION = '0.0.1';
+
+/**
+ * Hono instrumentation for OpenTelemetry
+ */
+export class HonoInstrumentation extends InstrumentationBase {
+ public constructor() {
+ super(PACKAGE_NAME, PACKAGE_VERSION, {});
+ }
+
+ /**
+ * Initialize the instrumentation.
+ */
+ public init(): InstrumentationNodeModuleDefinition[] {
+ return [
+ new InstrumentationNodeModuleDefinition('hono', ['>=4.0.0 <5'], moduleExports => this._patch(moduleExports)),
+ ];
+ }
+
+ /**
+ * Patches the module exports to instrument Hono.
+ */
+ private _patch(moduleExports: { Hono: Hono }): { Hono: Hono } {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const instrumentation = this;
+
+ moduleExports.Hono = class HonoWrapper extends moduleExports.Hono {
+ public constructor(...args: unknown[]) {
+ super(...args);
+
+ instrumentation._wrap(this, 'get', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'post', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'put', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'delete', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'options', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'patch', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'all', instrumentation._patchHandler());
+ instrumentation._wrap(this, 'on', instrumentation._patchOnHandler());
+ instrumentation._wrap(this, 'use', instrumentation._patchMiddlewareHandler());
+ }
+ };
+ return moduleExports;
+ }
+
+ /**
+ * Patches the route handler to instrument it.
+ */
+ private _patchHandler(): (original: HandlerInterface) => HandlerInterface {
+ return function (original: HandlerInterface) {
+ return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
+ // TODO: Add OpenTelemetry tracing logic here
+ return original.apply(this, args);
+ };
+ };
+ }
+
+ /**
+ * Patches the 'on' handler to instrument it.
+ */
+ private _patchOnHandler(): (original: OnHandlerInterface) => OnHandlerInterface {
+ return function (original: OnHandlerInterface) {
+ return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
+ // TODO: Add OpenTelemetry tracing logic here
+ return original.apply(this, args);
+ };
+ };
+ }
+
+ /**
+ * Patches the middleware handler to instrument it.
+ */
+ private _patchMiddlewareHandler(): (original: MiddlewareHandlerInterface) => MiddlewareHandlerInterface {
+ return function (original: MiddlewareHandlerInterface) {
+ return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
+ // TODO: Add OpenTelemetry tracing logic here
+ return original.apply(this, args);
+ };
+ };
+ }
+}
diff --git a/packages/node/src/integrations/tracing/hono/types.ts b/packages/node/src/integrations/tracing/hono/types.ts
new file mode 100644
index 000000000000..3d7e057859f1
--- /dev/null
+++ b/packages/node/src/integrations/tracing/hono/types.ts
@@ -0,0 +1,50 @@
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/request.ts#L30
+export type HonoRequest = {
+ path: string;
+};
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/context.ts#L291
+export type Context = {
+ req: HonoRequest;
+};
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L36C1-L36C39
+export type Next = () => Promise;
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L73
+export type Handler = (c: Context, next: Next) => Promise | Response;
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L80
+export type MiddlewareHandler = (c: Context, next: Next) => Promise | Response | void;
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L109
+export type HandlerInterface = {
+ (...handlers: (Handler | MiddlewareHandler)[]): HonoInstance;
+ (path: string, ...handlers: (Handler | MiddlewareHandler)[]): HonoInstance;
+};
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L1071
+export type OnHandlerInterface = {
+ (method: string | string[], path: string | string[], ...handlers: (Handler | MiddlewareHandler)[]): HonoInstance;
+};
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L679
+export type MiddlewareHandlerInterface = {
+ (...handlers: MiddlewareHandler[]): HonoInstance;
+ (path: string, ...handlers: MiddlewareHandler[]): HonoInstance;
+};
+
+// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/hono-base.ts#L99
+export interface HonoInstance {
+ get: HandlerInterface;
+ post: HandlerInterface;
+ put: HandlerInterface;
+ delete: HandlerInterface;
+ options: HandlerInterface;
+ patch: HandlerInterface;
+ all: HandlerInterface;
+ on: OnHandlerInterface;
+ use: MiddlewareHandlerInterface;
+}
+
+export type Hono = new (...args: unknown[]) => HonoInstance;
diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts
index 6035cf3669f8..2d660670d297 100644
--- a/packages/node/src/integrations/tracing/index.ts
+++ b/packages/node/src/integrations/tracing/index.ts
@@ -1,6 +1,7 @@
import type { Integration } from '@sentry/core';
import { instrumentOtelHttp } from '../http';
import { amqplibIntegration, instrumentAmqplib } from './amqplib';
+import { anthropicAIIntegration, instrumentAnthropicAi } from './anthropic-ai';
import { connectIntegration, instrumentConnect } from './connect';
import { expressIntegration, instrumentExpress } from './express';
import { fastifyIntegration, instrumentFastify, instrumentFastifyV3 } from './fastify';
@@ -50,6 +51,7 @@ export function getAutoPerformanceIntegrations(): Integration[] {
openAIIntegration(),
postgresJsIntegration(),
firebaseIntegration(),
+ anthropicAIIntegration(),
];
}
@@ -83,5 +85,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
instrumentOpenAi,
instrumentPostgresJs,
instrumentFirebase,
+ instrumentAnthropicAi,
];
}
diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts
index 308c8be29abe..8376c99c1998 100644
--- a/packages/node/src/integrations/tracing/redis.ts
+++ b/packages/node/src/integrations/tracing/redis.ts
@@ -75,13 +75,13 @@ const cacheResponseHook: RedisResponseCustomAttributeFunction = (span: Span, red
span.updateName(truncate(spanDescription, 1024));
};
-const instrumentIORedis = generateInstrumentOnce('IORedis', () => {
+const instrumentIORedis = generateInstrumentOnce(`${INTEGRATION_NAME}.IORedis`, () => {
return new IORedisInstrumentation({
responseHook: cacheResponseHook,
});
});
-const instrumentRedisModule = generateInstrumentOnce('Redis', () => {
+const instrumentRedisModule = generateInstrumentOnce(`${INTEGRATION_NAME}.Redis`, () => {
return new RedisInstrumentation({
responseHook: cacheResponseHook,
});
diff --git a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts
index 22ec18a682f0..0b66f7e80919 100644
--- a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts
+++ b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts
@@ -1,11 +1,16 @@
import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
+import type { Span } from '@sentry/core';
import {
+ _INTERNAL_cleanupToolCallSpan,
+ _INTERNAL_getSpanForToolCallId,
addNonEnumerableProperty,
+ captureException,
getActiveSpan,
getCurrentScope,
handleCallbackErrors,
SDK_VERSION,
+ withScope,
} from '@sentry/core';
import { INTEGRATION_NAME } from './constants';
import type { TelemetrySettings, VercelAiIntegration } from './types';
@@ -35,6 +40,98 @@ interface RecordingOptions {
recordOutputs?: boolean;
}
+interface ToolError {
+ type: 'tool-error' | 'tool-result' | 'tool-call';
+ toolCallId: string;
+ toolName: string;
+ input?: {
+ [key: string]: unknown;
+ };
+ error: Error;
+ dynamic?: boolean;
+}
+
+function isToolError(obj: unknown): obj is ToolError {
+ if (typeof obj !== 'object' || obj === null) {
+ return false;
+ }
+
+ const candidate = obj as Record;
+ return (
+ 'type' in candidate &&
+ 'error' in candidate &&
+ 'toolName' in candidate &&
+ 'toolCallId' in candidate &&
+ candidate.type === 'tool-error' &&
+ candidate.error instanceof Error
+ );
+}
+
+/**
+ * Check for tool errors in the result and capture them
+ * Tool errors are not rejected in Vercel V5, it is added as metadata to the result content
+ */
+function checkResultForToolErrors(result: unknown | Promise): void {
+ if (typeof result !== 'object' || result === null || !('content' in result)) {
+ return;
+ }
+
+ const resultObj = result as { content: Array
-# Official Sentry SDK for Nuxt (BETA)
+# Official Sentry SDK for Nuxt
[](https://www.npmjs.com/package/@sentry/nuxt)
[](https://www.npmjs.com/package/@sentry/nuxt)
[](https://www.npmjs.com/package/@sentry/nuxt)
-This SDK is in **Beta**. The API is stable but updates may include minor changes in behavior. Please reach out on
-[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback or concerns. This
-SDK is for [Nuxt](https://nuxt.com/). If you're using [Vue](https://vuejs.org/) see our
+This SDK is for [Nuxt](https://nuxt.com/). If you're using [Vue](https://vuejs.org/) see our
[Vue SDK here](https://github.com/getsentry/sentry-javascript/tree/develop/packages/vue).
## Links
@@ -21,17 +19,13 @@ SDK is for [Nuxt](https://nuxt.com/). If you're using [Vue](https://vuejs.org/)
## Compatibility
-The minimum supported version of Nuxt is `3.0.0`.
+The minimum supported version of Nuxt is `3.7.0` (`3.14.0+` recommended).
## General
This package is a wrapper around `@sentry/node` for the server and `@sentry/vue` for the client side, with added
functionality related to Nuxt.
-**Limitations:**
-
-- Server monitoring is not available during development mode (`nuxt dev`)
-
## Manual Setup
### 1. Prerequisites & Installation
@@ -112,20 +106,18 @@ Sentry.init({
## Uploading Source Maps
To upload source maps, you have to enable client source maps in your `nuxt.config.ts`. Then, you add your project
-settings to the `sentry.sourceMapsUploadOptions` of your `nuxt.config.ts`:
+settings to `sentry` in your `nuxt.config.ts`:
```javascript
// nuxt.config.ts
export default defineNuxtConfig({
- sourcemap: { client: true },
+ sourcemap: { client: 'hidden' },
modules: ['@sentry/nuxt/module'],
sentry: {
- sourceMapsUploadOptions: {
- org: 'your-org-slug',
- project: 'your-project-slug',
- authToken: process.env.SENTRY_AUTH_TOKEN,
- },
+ org: 'your-org-slug',
+ project: 'your-project-slug',
+ authToken: process.env.SENTRY_AUTH_TOKEN,
},
});
```
diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts
index 0e6d92636246..5e1343b1ebaa 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -112,15 +112,6 @@ export default defineNuxtModule({
nuxt.hooks.hook('nitro:init', nitro => {
if (serverConfigFile?.includes('.server.config')) {
- if (nitro.options.dev) {
- consoleSandbox(() => {
- // eslint-disable-next-line no-console
- console.log(
- '[Sentry] Your application is running in development mode. Note: @sentry/nuxt does not work as expected on the server-side (Nitro). Errors are reported, but tracing does not work.',
- );
- });
- }
-
consoleSandbox(() => {
const serverDir = nitro.options.output.serverDir;
@@ -154,8 +145,20 @@ export default defineNuxtModule({
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(
- `[Sentry] Using your \`${serverConfigFile}\` file for the server-side Sentry configuration. Make sure to add the Node option \`import\` to the Node command where you deploy and/or run your application. This preloads the Sentry configuration at server startup. You can do this via a command-line flag (\`node --import ${serverConfigRelativePath} [...]\`) or via an environment variable (\`NODE_OPTIONS='--import ${serverConfigRelativePath}' node [...]\`).`,
+ `[Sentry] Using \`${serverConfigFile}\` for server-side Sentry configuration. To activate Sentry on the Nuxt server-side, this file must be preloaded when starting your application. Make sure to add this where you deploy and/or run your application. Read more here: https://docs.sentry.io/platforms/javascript/guides/nuxt/install/.`,
);
+
+ if (nitro.options.dev) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `[Sentry] During development, preload Sentry with the NODE_OPTIONS environment variable: \`NODE_OPTIONS='--import ${serverConfigRelativePath}' nuxt dev\`. The file is generated in the build directory (usually '.nuxt'). If you delete the build directory, run \`nuxt dev\` to regenerate it.`,
+ );
+ } else {
+ // eslint-disable-next-line no-console
+ console.log(
+ `[Sentry] When running your built application, preload Sentry via a command-line flag (\`node --import ${serverConfigRelativePath} [...]\`) or via an environment variable (\`NODE_OPTIONS='--import ${serverConfigRelativePath}' node [...]\`).`,
+ );
+ }
});
}
}
diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts
index 7efe86b84587..626b574612b0 100644
--- a/packages/nuxt/test/server/sdk.test.ts
+++ b/packages/nuxt/test/server/sdk.test.ts
@@ -1,10 +1,9 @@
-import type { EventProcessor } from '@sentry/core';
-import type { NodeClient } from '@sentry/node';
+import type { Event, EventProcessor } from '@sentry/core';
import * as SentryNode from '@sentry/node';
import { getGlobalScope, Scope, SDK_VERSION } from '@sentry/node';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { init } from '../../src/server';
-import { clientSourceMapErrorFilter } from '../../src/server/sdk';
+import { clientSourceMapErrorFilter, lowQualityTransactionsFilter } from '../../src/server/sdk';
const nodeInit = vi.spyOn(SentryNode, 'init');
@@ -42,41 +41,43 @@ describe('Nuxt Server SDK', () => {
expect(init({})).not.toBeUndefined();
});
- describe('lowQualityTransactionsFilter (%s)', () => {
- const beforeSendEvent = vi.fn(event => event);
- const client = init({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- }) as NodeClient;
- client.on('beforeSendEvent', beforeSendEvent);
+ describe('lowQualityTransactionsFilter', () => {
+ const options = { debug: false };
+ const filter = lowQualityTransactionsFilter(options);
- it.each([
- [
+ describe('filters out low quality transactions', () => {
+ it.each([
'GET /_nuxt/some_asset.js',
'GET _nuxt/some_asset.js',
'GET /icons/favicon.ico',
'GET /assets/logo.png',
'GET /icons/zones/forest.svg',
- ],
- ])('filters out low quality transactions', async transaction => {
- client.captureEvent({ type: 'transaction', transaction });
- await client!.flush();
- expect(beforeSendEvent).not.toHaveBeenCalled();
+ ])('filters out low quality transaction: (%s)', transaction => {
+ const event = { type: 'transaction' as const, transaction };
+ expect(filter(event, {})).toBeNull();
+ });
});
- // Nuxt parametrizes routes sometimes in a special way - especially catchAll o.O
- it.each(['GET /', 'POST /_server', 'GET /catchAll/:id(.*)*', 'GET /article/:slug()', 'GET /user/:id'])(
- 'does not filter out high quality or route transactions (%s)',
- async transaction => {
- client.captureEvent({ type: 'transaction', transaction });
- await client!.flush();
- expect(beforeSendEvent).toHaveBeenCalledWith(
- expect.objectContaining({
- transaction,
- }),
- expect.any(Object),
- );
- },
- );
+ describe('keeps high quality transactions', () => {
+ // Nuxt parametrizes routes sometimes in a special way - especially catchAll o.O
+ it.each(['GET /', 'POST /_server', 'GET /catchAll/:id(.*)*', 'GET /article/:slug()', 'GET /user/:id'])(
+ 'does not filter out route transactions (%s)',
+ transaction => {
+ const event = { type: 'transaction' as const, transaction };
+ expect(filter(event, {})).toEqual(event);
+ },
+ );
+ });
+
+ it('does not filter non-transaction events', () => {
+ const event = { type: 'error' as const, transaction: 'GET /assets/image.png' } as unknown as Event;
+ expect(filter(event, {})).toEqual(event);
+ });
+
+ it('handles events without transaction property', () => {
+ const event = { type: 'transaction' as const };
+ expect(filter(event, {})).toEqual(event);
+ });
});
it('registers an event processor', async () => {
diff --git a/packages/react-router/src/server/integration/reactRouterServer.ts b/packages/react-router/src/server/integration/reactRouterServer.ts
index 89a0443c2382..4625d1cb979e 100644
--- a/packages/react-router/src/server/integration/reactRouterServer.ts
+++ b/packages/react-router/src/server/integration/reactRouterServer.ts
@@ -5,7 +5,7 @@ import { ReactRouterInstrumentation } from '../instrumentation/reactRouter';
const INTEGRATION_NAME = 'ReactRouterServer';
-const instrumentReactRouter = generateInstrumentOnce('React-Router-Server', () => {
+const instrumentReactRouter = generateInstrumentOnce(INTEGRATION_NAME, () => {
return new ReactRouterInstrumentation();
});
diff --git a/packages/replay-internal/test/integration/shouldFilterRequest.test.ts b/packages/replay-internal/test/integration/shouldFilterRequest.test.ts
index 355afe80060b..8e50fb75d608 100644
--- a/packages/replay-internal/test/integration/shouldFilterRequest.test.ts
+++ b/packages/replay-internal/test/integration/shouldFilterRequest.test.ts
@@ -20,6 +20,6 @@ describe('Integration | shouldFilterRequest', () => {
it('should filter requests for Sentry ingest URLs', async () => {
const { replay } = await mockSdk();
- expect(shouldFilterRequest(replay, 'https://03031aa.ingest.f00.f00/api/129312/')).toBe(true);
+ expect(shouldFilterRequest(replay, 'https://03031aa.ingest.f00.f00/api/129312/?sentry_key=123')).toBe(true);
});
});
diff --git a/packages/solidstart/src/server/utils.ts b/packages/solidstart/src/server/utils.ts
index 1560b254bd22..8276c32da9e0 100644
--- a/packages/solidstart/src/server/utils.ts
+++ b/packages/solidstart/src/server/utils.ts
@@ -17,25 +17,32 @@ export function isRedirect(error: unknown): boolean {
return hasValidLocation && hasValidStatus;
}
+/**
+ * Filter function for low quality transactions
+ *
+ * Exported only for tests
+ */
+export function lowQualityTransactionsFilter(options: Options): EventProcessor {
+ return Object.assign(
+ (event => {
+ if (event.type !== 'transaction') {
+ return event;
+ }
+ // Filter out transactions for build assets
+ if (event.transaction?.match(/^GET \/_build\//)) {
+ options.debug && debug.log('SolidStartLowQualityTransactionsFilter filtered transaction', event.transaction);
+ return null;
+ }
+ return event;
+ }) satisfies EventProcessor,
+ { id: 'SolidStartLowQualityTransactionsFilter' },
+ );
+}
+
/**
* Adds an event processor to filter out low quality transactions,
* e.g. to filter out transactions for build assets
*/
export function filterLowQualityTransactions(options: Options): void {
- getGlobalScope().addEventProcessor(
- Object.assign(
- (event => {
- if (event.type !== 'transaction') {
- return event;
- }
- // Filter out transactions for build assets
- if (event.transaction?.match(/^GET \/_build\//)) {
- options.debug && debug.log('SolidStartLowQualityTransactionsFilter filtered transaction', event.transaction);
- return null;
- }
- return event;
- }) satisfies EventProcessor,
- { id: 'SolidStartLowQualityTransactionsFilter' },
- ),
- );
+ getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options));
}
diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts
index b700b43a067a..c5df698ed307 100644
--- a/packages/solidstart/test/server/sdk.test.ts
+++ b/packages/solidstart/test/server/sdk.test.ts
@@ -1,8 +1,9 @@
-import type { NodeClient } from '@sentry/node';
-import { SDK_VERSION } from '@sentry/node';
+import type { EventProcessor } from '@sentry/core';
+import { getGlobalScope, Scope, SDK_VERSION } from '@sentry/node';
import * as SentryNode from '@sentry/node';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { init as solidStartInit } from '../../src/server';
+import { lowQualityTransactionsFilter } from '../../src/server/utils';
const browserInit = vi.spyOn(SentryNode, 'init');
@@ -34,37 +35,52 @@ describe('Initialize Solid Start SDK', () => {
expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata));
});
- it('filters out low quality transactions', async () => {
- const beforeSendEvent = vi.fn(event => event);
- const client = solidStartInit({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- }) as NodeClient;
- client.on('beforeSendEvent', beforeSendEvent);
+ describe('lowQualityTransactionsFilter', () => {
+ const options = { debug: false };
+ const filter = lowQualityTransactionsFilter(options);
+
+ describe('filters out low quality transactions', () => {
+ it.each(['GET /_build/some_asset.js', 'GET /_build/app.js', 'GET /_build/assets/logo.png'])(
+ 'filters out low quality transaction: (%s)',
+ transaction => {
+ const event = { type: 'transaction' as const, transaction };
+ expect(filter(event, {})).toBeNull();
+ },
+ );
+ });
+
+ describe('keeps high quality transactions', () => {
+ it.each(['GET /', 'POST /_server'])('does not filter out route transactions (%s)', transaction => {
+ const event = { type: 'transaction' as const, transaction };
+ expect(filter(event, {})).toEqual(event);
+ });
+ });
- client.captureEvent({ type: 'transaction', transaction: 'GET /' });
- client.captureEvent({ type: 'transaction', transaction: 'GET /_build/some_asset.js' });
- client.captureEvent({ type: 'transaction', transaction: 'POST /_server' });
+ it('does not filter non-transaction events', () => {
+ const event = { type: 'error' as const, transaction: 'GET /_build/app.js' } as any;
+ expect(filter(event, {})).toEqual(event);
+ });
+
+ it('handles events without transaction property', () => {
+ const event = { type: 'transaction' as const };
+ expect(filter(event, {})).toEqual(event);
+ });
+ });
- await client!.flush();
+ it('registers an event processor', () => {
+ let passedEventProcessors: EventProcessor[] = [];
+ const addEventProcessor = vi
+ .spyOn(getGlobalScope(), 'addEventProcessor')
+ .mockImplementation((eventProcessor: EventProcessor) => {
+ passedEventProcessors = [...passedEventProcessors, eventProcessor];
+ return new Scope();
+ });
+
+ solidStartInit({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ });
- expect(beforeSendEvent).toHaveBeenCalledTimes(2);
- expect(beforeSendEvent).toHaveBeenCalledWith(
- expect.objectContaining({
- transaction: 'GET /',
- }),
- expect.any(Object),
- );
- expect(beforeSendEvent).not.toHaveBeenCalledWith(
- expect.objectContaining({
- transaction: 'GET /_build/some_asset.js',
- }),
- expect.any(Object),
- );
- expect(beforeSendEvent).toHaveBeenCalledWith(
- expect.objectContaining({
- transaction: 'POST /_server',
- }),
- expect.any(Object),
- );
+ expect(addEventProcessor).toHaveBeenCalledTimes(1);
+ expect(passedEventProcessors[0]?.id).toEqual('SolidStartLowQualityTransactionsFilter');
});
});
diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts
index c1605568de2e..7dc67d0131ea 100644
--- a/packages/vercel-edge/test/wintercg-fetch.test.ts
+++ b/packages/vercel-edge/test/wintercg-fetch.test.ts
@@ -102,8 +102,8 @@ describe('WinterCGFetch instrumentation', () => {
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startHandlerData: HandlerDataFetch = {
- fetchData: { url: 'https://dsn.ingest.sentry.io/1337', method: 'POST' },
- args: ['https://dsn.ingest.sentry.io/1337'],
+ fetchData: { url: 'https://dsn.ingest.sentry.io/1337?sentry_key=123', method: 'POST' },
+ args: ['https://dsn.ingest.sentry.io/1337?sentry_key=123'],
startTimestamp: Date.now(),
};
fetchInstrumentationHandlerCallback(startHandlerData);
diff --git a/yarn.lock b/yarn.lock
index 4175e91b712c..16f91521a991 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5876,15 +5876,6 @@
"@opentelemetry/instrumentation" "^0.203.0"
"@opentelemetry/semantic-conventions" "^1.27.0"
-"@opentelemetry/instrumentation-aws-lambda@0.54.0":
- version "0.54.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.54.0.tgz#835263593aa988ec460e840d3d47110392aaf92e"
- integrity sha512-uiYI+kcMUJ/H9cxAwB8c9CaG8behLRgcYSOEA8M/tMQ54Y1ZmzAuEE3QKOi21/s30x5Q+by9g7BwiVfDtqzeMA==
- dependencies:
- "@opentelemetry/instrumentation" "^0.203.0"
- "@opentelemetry/semantic-conventions" "^1.27.0"
- "@types/aws-lambda" "8.10.150"
-
"@opentelemetry/instrumentation-aws-sdk@0.56.0":
version "0.56.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.56.0.tgz#a65cd88351b7bd8566413798764679295166754a"
@@ -6140,10 +6131,10 @@
"@opentelemetry/resources" "2.0.0"
"@opentelemetry/semantic-conventions" "^1.29.0"
-"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0":
- version "1.34.0"
- resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz#8b6a46681b38a4d5947214033ac48128328c1738"
- integrity sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA==
+"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0", "@opentelemetry/semantic-conventions@^1.33.1", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0":
+ version "1.36.0"
+ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz#149449bd4df4d0464220915ad4164121e0d75d4d"
+ integrity sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==
"@opentelemetry/sql-common@^0.41.0":
version "0.41.0"
@@ -7912,7 +7903,7 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
-"@types/aws-lambda@8.10.150", "@types/aws-lambda@^8.10.62":
+"@types/aws-lambda@^8.10.62":
version "8.10.150"
resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.150.tgz#4998b238750ec389a326a7cdb625808834036bd3"
integrity sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==