diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8a8397098d2..983de7416ed4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,49 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 9.33.0
+
+### Important Changes
+
+- **feat: Add opt-in `vercelAiIntegration` to cloudflare & vercel-edge ([#16732](https://github.com/getsentry/sentry-javascript/pull/16732))**
+
+The `vercelAiIntegration` is now available as opt-in for the Cloudflare and the Next.js SDK for Vercel Edge.
+To use it, add the integration in `Sentry.init`
+
+```js
+Sentry.init({
+ tracesSampleRate: 1.0,
+ integrations: [Sentry.vercelAIIntegration()],
+});
+```
+
+And enable telemetry for Vercel AI calls
+
+```js
+const result = await generateText({
+ model: openai('gpt-4o'),
+ experimental_telemetry: {
+ isEnabled: true,
+ },
+});
+```
+
+- **feat(node): Add postgresjs instrumentation ([#16665](https://github.com/getsentry/sentry-javascript/pull/16665))**
+
+The Node.js SDK now includes instrumentation for [Postgres.js](https://www.npmjs.com/package/postgres).
+
+- **feat(node): Use diagnostics channel for Fastify v5 error handling ([#16715](https://github.com/getsentry/sentry-javascript/pull/16715))**
+
+If you're on Fastify v5, you no longer need to call `setupFastifyErrorHandler`. It is done automatically by the node SDK. Older versions still rely on calling `setupFastifyErrorHandler`.
+
+### Other Changes
+
+- feat(cloudflare): Allow interop with OpenTelemetry emitted spans ([#16714](https://github.com/getsentry/sentry-javascript/pull/16714))
+- feat(cloudflare): Flush after `waitUntil` ([#16681](https://github.com/getsentry/sentry-javascript/pull/16681))
+- fix(nextjs): Remove `ai` from default server external packages ([#16736](https://github.com/getsentry/sentry-javascript/pull/16736))
+
+Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution!
+
## 9.32.0
### Important Changes
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js
new file mode 100644
index 000000000000..09af5f3e4ab4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js
@@ -0,0 +1,19 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+// Force this so that the initial sampleRand is consistent
+Math.random = () => 0.45;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [Sentry.browserTracingIntegration()],
+ tracePropagationTargets: ['http://sentry-test-site.example'],
+ tracesSampler: ({ name }) => {
+ if (name === 'new-trace') {
+ return 0.9;
+ }
+
+ return 0.5;
+ },
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/subject.js b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/subject.js
new file mode 100644
index 000000000000..711620998cb2
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/subject.js
@@ -0,0 +1,13 @@
+const newTraceBtn = document.getElementById('newTrace');
+newTraceBtn.addEventListener('click', async () => {
+ Sentry.startNewTrace(() => {
+ // We want to ensure the new trace is sampled, so we force the sample_rand to a value above 0.9
+ Sentry.getCurrentScope().setPropagationContext({
+ ...Sentry.getCurrentScope().getPropagationContext(),
+ sampleRand: 0.85,
+ });
+ Sentry.startSpan({ op: 'ui.interaction.click', name: 'new-trace' }, async () => {
+ await fetch('http://sentry-test-site.example');
+ });
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/template.html b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/template.html
new file mode 100644
index 000000000000..11b051919b55
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/template.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts
new file mode 100644
index 000000000000..616c89cd66f8
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/test.ts
@@ -0,0 +1,89 @@
+import { expect } from '@playwright/test';
+import { sentryTest } from '../../../../utils/fixtures';
+import type { EventAndTraceHeader } from '../../../../utils/helpers';
+import {
+ eventAndTraceHeaderRequestParser,
+ getFirstSentryEnvelopeRequest,
+ shouldSkipTracingTest,
+ waitForTransactionRequest,
+} from '../../../../utils/helpers';
+
+sentryTest(
+ 'new trace started with `startNewTrace` is sampled according to the `tracesSampler`',
+ async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ await page.route('http://sentry-test-site.example/**', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({}),
+ });
+ });
+
+ const [pageloadEvent, pageloadTraceHeaders] = await getFirstSentryEnvelopeRequest(
+ page,
+ url,
+ eventAndTraceHeaderRequestParser,
+ );
+
+ const pageloadTraceContext = pageloadEvent.contexts?.trace;
+
+ expect(pageloadEvent.type).toEqual('transaction');
+
+ expect(pageloadTraceContext).toMatchObject({
+ op: 'pageload',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ data: {
+ 'sentry.sample_rate': 0.5,
+ },
+ });
+ expect(pageloadTraceContext).not.toHaveProperty('parent_span_id');
+
+ expect(pageloadTraceHeaders).toEqual({
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '0.5',
+ sampled: 'true',
+ trace_id: pageloadTraceContext?.trace_id,
+ sample_rand: '0.45',
+ });
+
+ const transactionPromise = waitForTransactionRequest(page, event => {
+ return event.transaction === 'new-trace';
+ });
+
+ await page.locator('#newTrace').click();
+
+ const [newTraceTransactionEvent, newTraceTransactionTraceHeaders] = eventAndTraceHeaderRequestParser(
+ await transactionPromise,
+ );
+
+ const newTraceTransactionTraceContext = newTraceTransactionEvent.contexts?.trace;
+ expect(newTraceTransactionTraceContext).toMatchObject({
+ op: 'ui.interaction.click',
+ trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
+ span_id: expect.stringMatching(/^[0-9a-f]{16}$/),
+ data: {
+ 'sentry.sample_rate': 0.9,
+ },
+ });
+
+ expect(newTraceTransactionTraceHeaders).toEqual({
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '0.9',
+ sampled: 'true',
+ trace_id: newTraceTransactionTraceContext?.trace_id,
+ transaction: 'new-trace',
+ sample_rand: '0.85',
+ });
+
+ expect(newTraceTransactionTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);
+ },
+);
diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json
index 3e8416b3aaee..dffcecc3abf3 100644
--- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json
+++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json
@@ -8,7 +8,7 @@
"test:prod": "TEST_ENV=prod playwright test",
"test:dev": "TEST_ENV=dev playwright test",
"test:build": "pnpm install && pnpm build",
- "test:build-13": "pnpm install && pnpm add next@13.4.19 && pnpm build",
+ "test:build-13": "pnpm install && pnpm add next@13.5.9 && pnpm build",
"test:assert": "pnpm test:prod && pnpm test:dev"
},
"dependencies": {
@@ -16,7 +16,7 @@
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "14.0.0",
+ "next": "14.2.25",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "~5.0.0"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
index cb78ab4ecb4b..eea815ebb836 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json
@@ -17,7 +17,7 @@
"@types/node": "^18.19.1",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "13.5.7",
+ "next": "13.5.9",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "~5.0.0"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
index f02e3c0138da..a06b718835bf 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json
@@ -11,7 +11,7 @@
"test:test-build": "pnpm ts-node --script-mode assert-build.ts",
"test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build",
"test:build-latest": "pnpm install && pnpm add next@latest && pnpm build",
- "test:build-13": "pnpm install && pnpm add next@13.4.19 && pnpm build",
+ "test:build-13": "pnpm install && pnpm add next@13.5.9 && pnpm build",
"test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev"
},
"dependencies": {
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts
index 73ffafcfd04d..db2e9bf9cc5f 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts
@@ -34,8 +34,6 @@ const app = fastify();
const port = 3030;
const port2 = 3040;
-Sentry.setupFastifyErrorHandler(app);
-
app.get('/test-success', function (_req, res) {
res.send({ version: 'v1' });
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/app.vue
index 23283a522546..6550bbe08887 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/app.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/app.vue
@@ -13,5 +13,8 @@
-
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/composables/use-sentry-test-tag.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/app/composables/use-sentry-test-tag.ts
new file mode 100644
index 000000000000..0d6642ca3d8c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/composables/use-sentry-test-tag.ts
@@ -0,0 +1,8 @@
+// fixme: this needs to be imported from @sentry/core, not @sentry/nuxt in dev mode (because of import-in-the-middle error)
+// This could also be a problem with the specific setup of the pnpm E2E test setup, because this could not be reproduced outside of the E2E test.
+// Related to this: https://github.com/getsentry/sentry-javascript/issues/15204#issuecomment-2948908130
+import { setTag } from '@sentry/nuxt';
+
+export default function useSentryTestTag(): void {
+ setTag('test-tag', null);
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/modules/another-module.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/modules/another-module.ts
new file mode 100644
index 000000000000..9c1a3ca80487
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/modules/another-module.ts
@@ -0,0 +1,9 @@
+import { defineNuxtModule } from 'nuxt/kit';
+
+// Just a fake module to check if the SDK works alongside other local Nuxt modules without breaking the build
+export default defineNuxtModule({
+ meta: { name: 'another-module' },
+ setup() {
+ console.log('another-module setup called');
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
index da988a9ee003..ce3d681c963f 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
@@ -1,7 +1,7 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
future: { compatibilityVersion: 4 },
- compatibilityDate: '2024-04-03',
+ compatibilityDate: '2025-06-06',
imports: { autoImport: false },
modules: ['@pinia/nuxt', '@sentry/nuxt/module'],
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
index e3c7ec9c0a76..8e1074a04bc1 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -18,7 +18,7 @@
"dependencies": {
"@pinia/nuxt": "^0.5.5",
"@sentry/nuxt": "latest || *",
- "nuxt": "^3.13.2"
+ "nuxt": "^3.17.5"
},
"devDependencies": {
"@playwright/test": "~1.50.0",
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index 40acc7510fda..30a8f784b1e9 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -61,6 +61,7 @@
"node-cron": "^3.0.3",
"node-schedule": "^2.1.1",
"pg": "8.16.0",
+ "postgres": "^3.4.7",
"proxy": "^2.1.1",
"redis-4": "npm:redis@^4.6.14",
"reflect-metadata": "0.2.1",
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml
new file mode 100644
index 000000000000..301280106faa
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3.9'
+
+services:
+ db:
+ image: postgres:13
+ restart: always
+ container_name: integration-tests-postgresjs
+ ports:
+ - '5444:5432'
+ environment:
+ POSTGRES_USER: test
+ POSTGRES_PASSWORD: test
+ POSTGRES_DB: test_db
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js
new file mode 100644
index 000000000000..e7cb92aabf27
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/scenario.js
@@ -0,0 +1,62 @@
+const { loggingTransport } = require('@sentry-internal/node-integration-tests');
+const Sentry = require('@sentry/node');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+// Stop the process from exiting before the transaction is sent
+setInterval(() => {}, 1000);
+
+const postgres = require('postgres');
+
+const sql = postgres({ port: 5444, user: 'test', password: 'test', database: 'test_db' });
+
+async function run() {
+ await Sentry.startSpan(
+ {
+ name: 'Test Transaction',
+ op: 'transaction',
+ },
+ async () => {
+ try {
+ await sql`
+ CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));
+ `;
+
+ await sql`
+ INSERT INTO "User" ("email", "name") VALUES ('Foo', 'bar@baz.com');
+ `;
+
+ await sql`
+ UPDATE "User" SET "name" = 'Foo' WHERE "email" = 'bar@baz.com';
+ `;
+
+ await sql`
+ SELECT * FROM "User" WHERE "email" = 'bar@baz.com';
+ `;
+
+ await sql`SELECT * from generate_series(1,1000) as x `.cursor(10, async rows => {
+ await Promise.all(rows);
+ });
+
+ await sql`
+ DROP TABLE "User";
+ `;
+
+ // This will be captured as an error as the table no longer exists
+ await sql`
+ SELECT * FROM "User" WHERE "email" = 'foo@baz.com';
+ `;
+ } finally {
+ await sql.end();
+ }
+ },
+ );
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts
new file mode 100644
index 000000000000..68b1a82703a0
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/postgresjs/test.ts
@@ -0,0 +1,225 @@
+import { describe, expect, test } from 'vitest';
+import { createRunner } from '../../../utils/runner';
+
+const EXISTING_TEST_EMAIL = 'bar@baz.com';
+const NON_EXISTING_TEST_EMAIL = 'foo@baz.com';
+
+describe('postgresjs auto instrumentation', () => {
+ test('should auto-instrument `postgres` package', { timeout: 60_000 }, async () => {
+ const EXPECTED_TRANSACTION = {
+ transaction: 'Test Transaction',
+ spans: expect.arrayContaining([
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'CREATE TABLE',
+ 'db.query.text':
+ 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))',
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description:
+ 'CREATE TABLE "User" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(?) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"))',
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'SELECT',
+ 'db.query.text':
+ "select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid",
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description:
+ "select b.oid, b.typarray from pg_catalog.pg_type a left join pg_catalog.pg_type b on b.oid = a.typelem where a.typcategory = 'A' group by b.oid, b.typarray order by b.oid",
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'INSERT',
+ 'db.query.text': `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`,
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'sentry.op': 'db',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description: `INSERT INTO "User" ("email", "name") VALUES ('Foo', '${EXISTING_TEST_EMAIL}')`,
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'UPDATE',
+ 'db.query.text': `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description: `UPDATE "User" SET "name" = 'Foo' WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'SELECT',
+ 'db.query.text': `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description: `SELECT * FROM "User" WHERE "email" = '${EXISTING_TEST_EMAIL}'`,
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'SELECT',
+ 'db.query.text': 'SELECT * from generate_series(?,?) as x',
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description: 'SELECT * from generate_series(?,?) as x',
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ 'db.operation.name': 'DROP TABLE',
+ 'db.query.text': 'DROP TABLE "User"',
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description: 'DROP TABLE "User"',
+ op: 'db',
+ status: 'ok',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'db.namespace': 'test_db',
+ 'db.system.name': 'postgres',
+ // No db.operation.name here, as this is an errored span
+ 'db.response.status_code': '42P01',
+ 'error.type': 'PostgresError',
+ 'db.query.text': `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`,
+ 'sentry.op': 'db',
+ 'sentry.origin': 'auto.db.otel.postgres',
+ 'server.address': 'localhost',
+ 'server.port': 5444,
+ }),
+ description: `SELECT * FROM "User" WHERE "email" = '${NON_EXISTING_TEST_EMAIL}'`,
+ op: 'db',
+ status: 'unknown_error',
+ origin: 'auto.db.otel.postgres',
+ parent_span_id: expect.any(String),
+ span_id: expect.any(String),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ }),
+ ]),
+ };
+
+ const EXPECTED_ERROR_EVENT = {
+ event_id: expect.any(String),
+ contexts: {
+ trace: {
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ },
+ },
+ exception: {
+ values: [
+ {
+ type: 'PostgresError',
+ value: 'relation "User" does not exist',
+ stacktrace: expect.objectContaining({
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ function: 'handle',
+ module: 'postgres.cjs.src:connection',
+ filename: expect.any(String),
+ lineno: expect.any(Number),
+ colno: expect.any(Number),
+ }),
+ ]),
+ }),
+ },
+ ],
+ },
+ };
+
+ await createRunner(__dirname, 'scenario.js')
+ .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] })
+ .expect({ transaction: EXPECTED_TRANSACTION })
+ .expect({ event: EXPECTED_ERROR_EVENT })
+ .start()
+ .completed();
+ });
+});
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 750eb05d8b10..83a135e71f21 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -84,6 +84,7 @@ export {
onUnhandledRejectionIntegration,
parameterize,
postgresIntegration,
+ postgresJsIntegration,
prismaIntegration,
childProcessIntegration,
createSentryWinstonTransport,
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index b13f69a9b6ce..f64ee53dc47c 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -99,6 +99,7 @@ export {
redisIntegration,
tediousIntegration,
postgresIntegration,
+ postgresJsIntegration,
prismaIntegration,
childProcessIntegration,
createSentryWinstonTransport,
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 14a44e2d38fc..4a9d7fd9d71c 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -120,6 +120,7 @@ export {
redisIntegration,
tediousIntegration,
postgresIntegration,
+ postgresJsIntegration,
prismaIntegration,
hapiIntegration,
setupHapiErrorHandler,
diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json
index e3363708c70d..b9edb74a4231 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -49,7 +49,8 @@
"access": "public"
},
"dependencies": {
- "@sentry/core": "9.32.0"
+ "@sentry/core": "9.32.0",
+ "@opentelemetry/api": "^1.9.0"
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x"
diff --git a/packages/cloudflare/src/client.ts b/packages/cloudflare/src/client.ts
index 9b4f18086658..b6b4695835ba 100644
--- a/packages/cloudflare/src/client.ts
+++ b/packages/cloudflare/src/client.ts
@@ -1,5 +1,6 @@
import type { ClientOptions, Options, ServerRuntimeClientOptions } from '@sentry/core';
import { applySdkMetadata, ServerRuntimeClient } from '@sentry/core';
+import type { makeFlushLock } from './flush';
import type { CloudflareTransportOptions } from './transport';
/**
@@ -8,7 +9,9 @@ import type { CloudflareTransportOptions } from './transport';
* @see CloudflareClientOptions for documentation on configuration options.
* @see ServerRuntimeClient for usage documentation.
*/
-export class CloudflareClient extends ServerRuntimeClient {
+export class CloudflareClient extends ServerRuntimeClient {
+ private readonly _flushLock: ReturnType | void;
+
/**
* Creates a new Cloudflare SDK instance.
* @param options Configuration options for this SDK.
@@ -16,9 +19,10 @@ export class CloudflareClient extends ServerRuntimeClient} A promise that resolves to a boolean indicating whether the flush operation was successful.
+ */
+ public async flush(timeout?: number): Promise {
+ if (this._flushLock) {
+ await this._flushLock.finalize();
+ }
+ return super.flush(timeout);
}
}
@@ -36,6 +55,19 @@ interface BaseCloudflareOptions {
* @default true
*/
enableDedupe?: boolean;
+
+ /**
+ * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
+ * via a custom trace provider.
+ * This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
+ * HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
+ * This should be good enough for many, but not all integrations.
+ *
+ * If you want to opt-out of setting up the OpenTelemetry compatibility tracer, set this to `true`.
+ *
+ * @default false
+ */
+ skipOpenTelemetrySetup?: boolean;
}
/**
@@ -43,11 +75,15 @@ interface BaseCloudflareOptions {
*
* @see @sentry/core Options for more information.
*/
-export interface CloudflareOptions extends Options, BaseCloudflareOptions {}
+export interface CloudflareOptions extends Options, BaseCloudflareOptions {
+ ctx?: ExecutionContext;
+}
/**
* Configuration options for the Sentry Cloudflare SDK Client class
*
* @see CloudflareClient for more information.
*/
-export interface CloudflareClientOptions extends ClientOptions, BaseCloudflareOptions {}
+export interface CloudflareClientOptions extends ClientOptions, BaseCloudflareOptions {
+ flushLock?: ReturnType;
+}
diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts
index 35fbb5096a41..0e919977025d 100644
--- a/packages/cloudflare/src/durableobject.ts
+++ b/packages/cloudflare/src/durableobject.ts
@@ -47,9 +47,11 @@ function wrapMethodWithSentry any>(
// see: https://github.com/getsentry/sentry-javascript/issues/13217
const context = wrapperOptions.context as ExecutionContext | undefined;
+ const waitUntil = context?.waitUntil?.bind?.(context);
+
const currentClient = scope.getClient();
if (!currentClient) {
- const client = init(wrapperOptions.options);
+ const client = init({ ...wrapperOptions.options, ctx: context });
scope.setClient(client);
}
@@ -68,7 +70,7 @@ function wrapMethodWithSentry any>(
});
throw e;
} finally {
- context?.waitUntil(flush(2000));
+ waitUntil?.(flush(2000));
}
}
@@ -92,7 +94,7 @@ function wrapMethodWithSentry any>(
});
throw e;
} finally {
- context?.waitUntil(flush(2000));
+ waitUntil?.(flush(2000));
}
});
});
diff --git a/packages/cloudflare/src/flush.ts b/packages/cloudflare/src/flush.ts
new file mode 100644
index 000000000000..f38c805d0f8b
--- /dev/null
+++ b/packages/cloudflare/src/flush.ts
@@ -0,0 +1,38 @@
+import type { ExecutionContext } from '@cloudflare/workers-types';
+
+type FlushLock = {
+ readonly ready: Promise;
+ readonly finalize: () => Promise;
+};
+
+/**
+ * Enhances the given execution context by wrapping its `waitUntil` method with a proxy
+ * to monitor pending tasks, and provides a flusher function to ensure all tasks
+ * have been completed before executing any subsequent logic.
+ *
+ * @param {ExecutionContext} context - The execution context to be enhanced. If no context is provided, the function returns undefined.
+ * @return {FlushLock} Returns a flusher function if a valid context is provided, otherwise undefined.
+ */
+export function makeFlushLock(context: ExecutionContext): FlushLock {
+ let resolveAllDone: () => void = () => undefined;
+ const allDone = new Promise(res => {
+ resolveAllDone = res;
+ });
+ let pending = 0;
+ const originalWaitUntil = context.waitUntil.bind(context) as typeof context.waitUntil;
+ context.waitUntil = promise => {
+ pending++;
+ return originalWaitUntil(
+ promise.finally(() => {
+ if (--pending === 0) resolveAllDone();
+ }),
+ );
+ };
+ return Object.freeze({
+ ready: allDone,
+ finalize: () => {
+ if (pending === 0) resolveAllDone();
+ return allDone;
+ },
+ });
+}
diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts
index d3d1f80dbbd5..3640d3cf7229 100644
--- a/packages/cloudflare/src/handler.ts
+++ b/packages/cloudflare/src/handler.ts
@@ -74,8 +74,9 @@ export function withSentry {
const options = getFinalOptions(optionsCallback(env), env);
+ const waitUntil = context.waitUntil.bind(context);
- const client = init(options);
+ const client = init({ ...options, ctx: context });
isolationScope.setClient(client);
addCloudResourceContext(isolationScope);
@@ -99,7 +100,7 @@ export function withSentry {
const options = getFinalOptions(optionsCallback(env), env);
+ const waitUntil = context.waitUntil.bind(context);
- const client = init(options);
+ const client = init({ ...options, ctx: context });
isolationScope.setClient(client);
addCloudResourceContext(isolationScope);
@@ -139,7 +141,7 @@ export function withSentry {
const options = getFinalOptions(optionsCallback(env), env);
+ const waitUntil = context.waitUntil.bind(context);
- const client = init(options);
+ const client = init({ ...options, ctx: context });
isolationScope.setClient(client);
addCloudResourceContext(isolationScope);
@@ -185,7 +188,7 @@ export function withSentry {
const options = getFinalOptions(optionsCallback(env), env);
- const client = init(options);
+ const waitUntil = context.waitUntil.bind(context);
+
+ const client = init({ ...options, ctx: context });
isolationScope.setClient(client);
addCloudResourceContext(isolationScope);
@@ -215,7 +220,7 @@ export function withSentry {
+ return {
+ name: INTEGRATION_NAME,
+ setup(client) {
+ addVercelAiProcessors(client);
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library.
+ * This integration is not enabled by default, you need to manually add it.
+ *
+ * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry).
+ *
+ * You need to enable collecting spans for a specific call by setting
+ * `experimental_telemetry.isEnabled` to `true` in the first argument of the function call.
+ *
+ * ```javascript
+ * const result = await generateText({
+ * model: openai('gpt-4-turbo'),
+ * experimental_telemetry: { isEnabled: true },
+ * });
+ * ```
+ *
+ * If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each
+ * function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs`
+ * to `true`.
+ *
+ * ```javascript
+ * const result = await generateText({
+ * model: openai('gpt-4-turbo'),
+ * experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true },
+ * });
+ */
+export const vercelAIIntegration = defineIntegration(_vercelAIIntegration);
diff --git a/packages/cloudflare/src/opentelemetry/tracer.ts b/packages/cloudflare/src/opentelemetry/tracer.ts
new file mode 100644
index 000000000000..94dc917c5070
--- /dev/null
+++ b/packages/cloudflare/src/opentelemetry/tracer.ts
@@ -0,0 +1,81 @@
+import type { Context, Span, SpanOptions, Tracer, TracerProvider } from '@opentelemetry/api';
+import { trace } from '@opentelemetry/api';
+import { startInactiveSpan, startSpanManual } from '@sentry/core';
+
+/**
+ * Set up a mock OTEL tracer to allow inter-op with OpenTelemetry emitted spans.
+ * This is not perfect but handles easy/common use cases.
+ */
+export function setupOpenTelemetryTracer(): void {
+ trace.setGlobalTracerProvider(new SentryCloudflareTraceProvider());
+}
+
+class SentryCloudflareTraceProvider implements TracerProvider {
+ private readonly _tracers: Map = new Map();
+
+ public getTracer(name: string, version?: string, options?: { schemaUrl?: string }): Tracer {
+ const key = `${name}@${version || ''}:${options?.schemaUrl || ''}`;
+ if (!this._tracers.has(key)) {
+ this._tracers.set(key, new SentryCloudflareTracer());
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return this._tracers.get(key)!;
+ }
+}
+
+class SentryCloudflareTracer implements Tracer {
+ public startSpan(name: string, options?: SpanOptions): Span {
+ return startInactiveSpan({
+ name,
+ ...options,
+ attributes: {
+ ...options?.attributes,
+ 'sentry.cloudflare_tracer': true,
+ },
+ });
+ }
+
+ /**
+ * NOTE: This does not handle `context` being passed in. It will always put spans on the current scope.
+ */
+ public startActiveSpan unknown>(name: string, fn: F): ReturnType;
+ public startActiveSpan unknown>(name: string, options: SpanOptions, fn: F): ReturnType;
+ public startActiveSpan unknown>(
+ name: string,
+ options: SpanOptions,
+ context: Context,
+ fn: F,
+ ): ReturnType;
+ public startActiveSpan unknown>(
+ name: string,
+ options: unknown,
+ context?: unknown,
+ fn?: F,
+ ): ReturnType {
+ const opts = (typeof options === 'object' && options !== null ? options : {}) as SpanOptions;
+
+ const spanOpts = {
+ name,
+ ...opts,
+ attributes: {
+ ...opts.attributes,
+ 'sentry.cloudflare_tracer': true,
+ },
+ };
+
+ const callback = (
+ typeof options === 'function'
+ ? options
+ : typeof context === 'function'
+ ? context
+ : typeof fn === 'function'
+ ? fn
+ : // eslint-disable-next-line @typescript-eslint/no-empty-function
+ () => {}
+ ) as F;
+
+ // In OTEL the semantic matches `startSpanManual` because spans are not auto-ended
+ return startSpanManual(spanOpts, callback) as ReturnType;
+ }
+}
diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts
index f1905609fb94..d403501f3ed4 100644
--- a/packages/cloudflare/src/request.ts
+++ b/packages/cloudflare/src/request.ts
@@ -35,7 +35,9 @@ export function wrapRequestHandler(
// see: https://github.com/getsentry/sentry-javascript/issues/13217
const context = wrapperOptions.context as ExecutionContext | undefined;
- const client = init(options);
+ const waitUntil = context?.waitUntil?.bind?.(context);
+
+ const client = init({ ...options, ctx: context });
isolationScope.setClient(client);
const urlObject = parseStringToURLObject(request.url);
@@ -65,7 +67,7 @@ export function wrapRequestHandler(
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
throw e;
} finally {
- context?.waitUntil(flush(2000));
+ waitUntil?.(flush(2000));
}
}
@@ -89,7 +91,7 @@ export function wrapRequestHandler(
captureException(e, { mechanism: { handled: false, type: 'cloudflare' } });
throw e;
} finally {
- context?.waitUntil(flush(2000));
+ waitUntil?.(flush(2000));
}
},
);
diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts
index 90ef3c0bedf9..58adf8f4e145 100644
--- a/packages/cloudflare/src/sdk.ts
+++ b/packages/cloudflare/src/sdk.ts
@@ -12,7 +12,9 @@ import {
} from '@sentry/core';
import type { CloudflareClientOptions, CloudflareOptions } from './client';
import { CloudflareClient } from './client';
+import { makeFlushLock } from './flush';
import { fetchIntegration } from './integrations/fetch';
+import { setupOpenTelemetryTracer } from './opentelemetry/tracer';
import { makeCloudflareTransport } from './transport';
import { defaultStackParser } from './vendor/stacktrace';
@@ -43,12 +45,27 @@ export function init(options: CloudflareOptions): CloudflareClient | undefined {
options.defaultIntegrations = getDefaultIntegrations(options);
}
+ const flushLock = options.ctx ? makeFlushLock(options.ctx) : undefined;
+ delete options.ctx;
+
const clientOptions: CloudflareClientOptions = {
...options,
stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
integrations: getIntegrationsToSetup(options),
transport: options.transport || makeCloudflareTransport,
+ flushLock,
};
+ /**
+ * The Cloudflare SDK is not OpenTelemetry native, however, we set up some OpenTelemetry compatibility
+ * via a custom trace provider.
+ * This ensures that any spans emitted via `@opentelemetry/api` will be captured by Sentry.
+ * HOWEVER, big caveat: This does not handle custom context handling, it will always work off the current scope.
+ * This should be good enough for many, but not all integrations.
+ */
+ if (!options.skipOpenTelemetrySetup) {
+ setupOpenTelemetryTracer();
+ }
+
return initAndBind(CloudflareClient, clientOptions) as CloudflareClient;
}
diff --git a/packages/cloudflare/src/utils/addOriginToSpan.ts b/packages/cloudflare/src/utils/addOriginToSpan.ts
new file mode 100644
index 000000000000..2a23710fa7cf
--- /dev/null
+++ b/packages/cloudflare/src/utils/addOriginToSpan.ts
@@ -0,0 +1,8 @@
+import type { Span } from '@opentelemetry/api';
+import type { SpanOrigin } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+
+/** Adds an origin to an OTEL Span. */
+export function addOriginToSpan(span: Span, origin: SpanOrigin): void {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, origin);
+}
diff --git a/packages/cloudflare/src/utils/commonjs.ts b/packages/cloudflare/src/utils/commonjs.ts
new file mode 100644
index 000000000000..23a9b97f9fc1
--- /dev/null
+++ b/packages/cloudflare/src/utils/commonjs.ts
@@ -0,0 +1,8 @@
+/** Detect CommonJS. */
+export function isCjs(): boolean {
+ try {
+ return typeof module !== 'undefined' && typeof module.exports !== 'undefined';
+ } catch {
+ return false;
+ }
+}
diff --git a/packages/cloudflare/test/durableobject.test.ts b/packages/cloudflare/test/durableobject.test.ts
new file mode 100644
index 000000000000..40d33741658e
--- /dev/null
+++ b/packages/cloudflare/test/durableobject.test.ts
@@ -0,0 +1,55 @@
+import type { ExecutionContext } from '@cloudflare/workers-types';
+import * as SentryCore from '@sentry/core';
+import { describe, expect, it, onTestFinished, vi } from 'vitest';
+import { instrumentDurableObjectWithSentry } from '../src/durableobject';
+import { isInstrumented } from '../src/instrument';
+
+describe('durable object', () => {
+ it('instrumentDurableObjectWithSentry generic functionality', () => {
+ const options = vi.fn();
+ const instrumented = instrumentDurableObjectWithSentry(options, vi.fn());
+ expect(instrumented).toBeTypeOf('function');
+ expect(() => Reflect.construct(instrumented, [])).not.toThrow();
+ expect(options).toHaveBeenCalledOnce();
+ });
+ it('all available durable object methods are instrumented', () => {
+ const testClass = vi.fn(() => ({
+ customMethod: vi.fn(),
+ fetch: vi.fn(),
+ alarm: vi.fn(),
+ webSocketMessage: vi.fn(),
+ webSocketClose: vi.fn(),
+ webSocketError: vi.fn(),
+ }));
+ const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any);
+ const dObject: any = Reflect.construct(instrumented, []);
+ for (const method of Object.getOwnPropertyNames(dObject)) {
+ expect(isInstrumented(dObject[method]), `Method ${method} is instrumented`).toBeTruthy();
+ }
+ });
+ it('flush performs after all waitUntil promises are finished', async () => {
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ const waitUntil = vi.fn();
+ const testClass = vi.fn(context => ({
+ fetch: () => {
+ context.waitUntil(new Promise(res => setTimeout(res)));
+ return new Response('test');
+ },
+ }));
+ const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any);
+ const context = {
+ waitUntil,
+ } as unknown as ExecutionContext;
+ const dObject: any = Reflect.construct(instrumented, [context, {} as any]);
+ expect(() => dObject.fetch(new Request('https://example.com'))).not.toThrow();
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toHaveBeenCalledOnce();
+ vi.advanceTimersToNextTimer();
+ await Promise.all(waitUntil.mock.calls.map(([p]) => p));
+ expect(flush).toBeCalled();
+ });
+});
diff --git a/packages/cloudflare/test/flush.test.ts b/packages/cloudflare/test/flush.test.ts
new file mode 100644
index 000000000000..34714711c682
--- /dev/null
+++ b/packages/cloudflare/test/flush.test.ts
@@ -0,0 +1,30 @@
+import { type ExecutionContext } from '@cloudflare/workers-types';
+import { describe, expect, it, onTestFinished, vi } from 'vitest';
+import { makeFlushLock } from '../src/flush';
+
+describe('Flush buffer test', () => {
+ const waitUntilPromises: Promise[] = [];
+ const mockExecutionContext: ExecutionContext = {
+ waitUntil: vi.fn(prmise => {
+ waitUntilPromises.push(prmise);
+ }),
+ passThroughOnException: vi.fn(),
+ };
+ it('should flush buffer immediately if no waitUntil were called', async () => {
+ const { finalize } = makeFlushLock(mockExecutionContext);
+ await expect(finalize()).resolves.toBeUndefined();
+ });
+ it('should flush buffer only after all waitUntil were finished', async () => {
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const task = new Promise(resolve => setTimeout(resolve, 100));
+ const lock = makeFlushLock(mockExecutionContext);
+ mockExecutionContext.waitUntil(task);
+ void lock.finalize();
+ vi.advanceTimersToNextTimer();
+ await Promise.all(waitUntilPromises);
+ await expect(lock.ready).resolves.toBeUndefined();
+ });
+});
diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts
index bced0fdbe277..2e5c0f836e89 100644
--- a/packages/cloudflare/test/handler.test.ts
+++ b/packages/cloudflare/test/handler.test.ts
@@ -1,10 +1,16 @@
// Note: These tests run the handler in Node.js, which has some differences to the cloudflare workers runtime.
// Although this is not ideal, this is the best we can do until we have a better way to test cloudflare workers.
-import type { ForwardableEmailMessage, MessageBatch, ScheduledController, TraceItem } from '@cloudflare/workers-types';
+import type {
+ ExecutionContext,
+ ForwardableEmailMessage,
+ MessageBatch,
+ ScheduledController,
+ TraceItem,
+} from '@cloudflare/workers-types';
import type { Event } from '@sentry/core';
import * as SentryCore from '@sentry/core';
-import { beforeEach, describe, expect, test, vi } from 'vitest';
+import { beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest';
import { CloudflareClient } from '../src/client';
import { withSentry } from '../src/handler';
import { markAsInstrumented } from '../src/instrument';
@@ -24,6 +30,10 @@ const MOCK_ENV = {
SENTRY_RELEASE: '1.1.1',
};
+function addDelayedWaitUntil(context: ExecutionContext) {
+ context.waitUntil(new Promise(resolve => setTimeout(() => resolve())));
+}
+
describe('withSentry', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -122,6 +132,32 @@ describe('withSentry', () => {
expect(sentryEvent.release).toEqual('2.0.0');
});
+
+ test('flush must be called when all waitUntil are done', async () => {
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const handler = {
+ fetch(_request, _env, _context) {
+ addDelayedWaitUntil(_context);
+ return new Response('test');
+ },
+ } satisfies ExportedHandler;
+
+ const wrappedHandler = withSentry(vi.fn(), handler);
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn(promise => waits.push(promise));
+ await wrappedHandler.fetch?.(new Request('https://example.com'), MOCK_ENV, {
+ waitUntil,
+ } as unknown as ExecutionContext);
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toBeCalled();
+ vi.advanceTimersToNextTimer().runAllTimers();
+ await Promise.all(waits);
+ expect(flush).toHaveBeenCalledOnce();
+ });
});
describe('scheduled handler', () => {
@@ -198,13 +234,12 @@ describe('withSentry', () => {
} satisfies ExportedHandler;
const context = createMockExecutionContext();
+ const waitUntilSpy = vi.spyOn(context, 'waitUntil');
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, context);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenCalledTimes(1);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(waitUntilSpy).toHaveBeenCalledTimes(1);
+ expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise));
});
test('creates a cloudflare client and sets it on the handler', async () => {
@@ -337,6 +372,32 @@ describe('withSentry', () => {
});
});
});
+
+ test('flush must be called when all waitUntil are done', async () => {
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const handler = {
+ scheduled(_controller, _env, _context) {
+ addDelayedWaitUntil(_context);
+ return;
+ },
+ } satisfies ExportedHandler;
+
+ const wrappedHandler = withSentry(vi.fn(), handler);
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn(promise => waits.push(promise));
+ await wrappedHandler.scheduled?.(createMockScheduledController(), MOCK_ENV, {
+ waitUntil,
+ } as unknown as ExecutionContext);
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toBeCalled();
+ vi.advanceTimersToNextTimer().runAllTimers();
+ await Promise.all(waits);
+ expect(flush).toHaveBeenCalledOnce();
+ });
});
describe('email handler', () => {
@@ -413,13 +474,12 @@ describe('withSentry', () => {
} satisfies ExportedHandler;
const context = createMockExecutionContext();
+ const waitUntilSpy = vi.spyOn(context, 'waitUntil');
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, context);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenCalledTimes(1);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(waitUntilSpy).toHaveBeenCalledTimes(1);
+ expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise));
});
test('creates a cloudflare client and sets it on the handler', async () => {
@@ -551,6 +611,32 @@ describe('withSentry', () => {
});
});
});
+
+ test('flush must be called when all waitUntil are done', async () => {
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const handler = {
+ email(_controller, _env, _context) {
+ addDelayedWaitUntil(_context);
+ return;
+ },
+ } satisfies ExportedHandler;
+
+ const wrappedHandler = withSentry(vi.fn(), handler);
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn(promise => waits.push(promise));
+ await wrappedHandler.email?.(createMockEmailMessage(), MOCK_ENV, {
+ waitUntil,
+ } as unknown as ExecutionContext);
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toBeCalled();
+ vi.advanceTimersToNextTimer().runAllTimers();
+ await Promise.all(waits);
+ expect(flush).toHaveBeenCalledOnce();
+ });
});
describe('queue handler', () => {
@@ -627,13 +713,12 @@ describe('withSentry', () => {
} satisfies ExportedHandler;
const context = createMockExecutionContext();
+ const waitUntilSpy = vi.spyOn(context, 'waitUntil');
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, context);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenCalledTimes(1);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(waitUntilSpy).toHaveBeenCalledTimes(1);
+ expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise));
});
test('creates a cloudflare client and sets it on the handler', async () => {
@@ -769,6 +854,32 @@ describe('withSentry', () => {
});
});
});
+
+ test('flush must be called when all waitUntil are done', async () => {
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const handler = {
+ queue(_controller, _env, _context) {
+ addDelayedWaitUntil(_context);
+ return;
+ },
+ } satisfies ExportedHandler;
+
+ const wrappedHandler = withSentry(vi.fn(), handler);
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn(promise => waits.push(promise));
+ await wrappedHandler.queue?.(createMockQueueBatch(), MOCK_ENV, {
+ waitUntil,
+ } as unknown as ExecutionContext);
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toBeCalled();
+ vi.advanceTimersToNextTimer().runAllTimers();
+ await Promise.all(waits);
+ expect(flush).toHaveBeenCalledOnce();
+ });
});
describe('tail handler', () => {
@@ -845,13 +956,12 @@ describe('withSentry', () => {
} satisfies ExportedHandler;
const context = createMockExecutionContext();
+ const waitUntilSpy = vi.spyOn(context, 'waitUntil');
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, context);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenCalledTimes(1);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(waitUntilSpy).toHaveBeenCalledTimes(1);
+ expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise));
});
test('creates a cloudflare client and sets it on the handler', async () => {
@@ -941,6 +1051,33 @@ describe('withSentry', () => {
expect(thrownError).toBe(error);
});
});
+
+ test('flush must be called when all waitUntil are done', async () => {
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ flush.mockRestore();
+ });
+ const handler = {
+ tail(_controller, _env, _context) {
+ addDelayedWaitUntil(_context);
+ return;
+ },
+ } satisfies ExportedHandler;
+
+ const wrappedHandler = withSentry(vi.fn(), handler);
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn(promise => waits.push(promise));
+ await wrappedHandler.tail?.(createMockTailEvent(), MOCK_ENV, {
+ waitUntil,
+ } as unknown as ExecutionContext);
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toBeCalled();
+ vi.advanceTimersToNextTimer().runAllTimers();
+ await Promise.all(waits);
+ expect(flush).toHaveBeenCalledOnce();
+ });
});
describe('hono errorHandler', () => {
diff --git a/packages/cloudflare/test/integrations/fetch.test.ts b/packages/cloudflare/test/integrations/fetch.test.ts
index 795b3e8c931c..724ff39c7dde 100644
--- a/packages/cloudflare/test/integrations/fetch.test.ts
+++ b/packages/cloudflare/test/integrations/fetch.test.ts
@@ -1,6 +1,5 @@
import type { HandlerDataFetch, Integration } from '@sentry/core';
import * as sentryCore from '@sentry/core';
-import * as sentryUtils from '@sentry/core';
import { createStackParser } from '@sentry/core';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CloudflareClient } from '../../src/client';
@@ -12,7 +11,7 @@ class FakeClient extends CloudflareClient {
}
}
-const addFetchInstrumentationHandlerSpy = vi.spyOn(sentryUtils, 'addFetchInstrumentationHandler');
+const addFetchInstrumentationHandlerSpy = vi.spyOn(sentryCore, 'addFetchInstrumentationHandler');
const instrumentFetchRequestSpy = vi.spyOn(sentryCore, 'instrumentFetchRequest');
const addBreadcrumbSpy = vi.spyOn(sentryCore, 'addBreadcrumb');
diff --git a/packages/cloudflare/test/opentelemetry.test.ts b/packages/cloudflare/test/opentelemetry.test.ts
new file mode 100644
index 000000000000..f918afff90cc
--- /dev/null
+++ b/packages/cloudflare/test/opentelemetry.test.ts
@@ -0,0 +1,145 @@
+import { trace } from '@opentelemetry/api';
+import type { TransactionEvent } from '@sentry/core';
+import { startSpan } from '@sentry/core';
+import { beforeEach, describe, expect, test } from 'vitest';
+import { init } from '../src/sdk';
+import { resetSdk } from './testUtils';
+
+describe('opentelemetry compatibility', () => {
+ beforeEach(() => {
+ resetSdk();
+ });
+
+ test('should not capture spans emitted via @opentelemetry/api when skipOpenTelemetrySetup is true', async () => {
+ const transactionEvents: TransactionEvent[] = [];
+
+ const client = init({
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ skipOpenTelemetrySetup: true,
+ beforeSendTransaction: event => {
+ transactionEvents.push(event);
+ return null;
+ },
+ });
+
+ const tracer = trace.getTracer('test');
+ const span = tracer.startSpan('test');
+ span.end();
+
+ await client!.flush();
+
+ tracer.startActiveSpan('test 2', { attributes: { 'test.attribute': 'test' } }, span2 => {
+ const span = tracer.startSpan('test 3', { attributes: { 'test.attribute': 'test2' } });
+ span.end();
+ span2.end();
+ });
+
+ await client!.flush();
+
+ expect(transactionEvents).toHaveLength(0);
+ });
+
+ test('should capture spans emitted via @opentelemetry/api', async () => {
+ const transactionEvents: TransactionEvent[] = [];
+
+ const client = init({
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ beforeSendTransaction: event => {
+ transactionEvents.push(event);
+ return null;
+ },
+ });
+
+ const tracer = trace.getTracer('test');
+ const span = tracer.startSpan('test');
+ span.end();
+
+ await client!.flush();
+
+ tracer.startActiveSpan('test 2', { attributes: { 'test.attribute': 'test' } }, span2 => {
+ const span = tracer.startSpan('test 3', { attributes: { 'test.attribute': 'test2' } });
+ span.end();
+ span2.end();
+ });
+
+ await client!.flush();
+
+ expect(transactionEvents).toHaveLength(2);
+ const [transactionEvent, transactionEvent2] = transactionEvents;
+
+ expect(transactionEvent?.spans?.length).toBe(0);
+ expect(transactionEvent?.transaction).toBe('test');
+ expect(transactionEvent?.contexts?.trace?.data).toEqual({
+ 'sentry.cloudflare_tracer': true,
+ 'sentry.origin': 'manual',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ });
+
+ expect(transactionEvent2?.spans?.length).toBe(1);
+ expect(transactionEvent2?.transaction).toBe('test 2');
+ expect(transactionEvent2?.contexts?.trace?.data).toEqual({
+ 'sentry.cloudflare_tracer': true,
+ 'sentry.origin': 'manual',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ 'test.attribute': 'test',
+ });
+
+ expect(transactionEvent2?.spans).toEqual([
+ expect.objectContaining({
+ description: 'test 3',
+ data: {
+ 'sentry.cloudflare_tracer': true,
+ 'sentry.origin': 'manual',
+ 'test.attribute': 'test2',
+ },
+ }),
+ ]);
+ });
+
+ test('opentelemetry spans should interop with Sentry spans', async () => {
+ const transactionEvents: TransactionEvent[] = [];
+
+ const client = init({
+ dsn: 'https://username@domain/123',
+ tracesSampleRate: 1,
+ beforeSendTransaction: event => {
+ transactionEvents.push(event);
+ return null;
+ },
+ });
+
+ const tracer = trace.getTracer('test');
+
+ startSpan({ name: 'sentry span' }, () => {
+ const span = tracer.startSpan('otel span');
+ span.end();
+ });
+
+ await client!.flush();
+
+ expect(transactionEvents).toHaveLength(1);
+ const [transactionEvent] = transactionEvents;
+
+ expect(transactionEvent?.spans?.length).toBe(1);
+ expect(transactionEvent?.transaction).toBe('sentry span');
+ expect(transactionEvent?.contexts?.trace?.data).toEqual({
+ 'sentry.origin': 'manual',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'custom',
+ });
+
+ expect(transactionEvent?.spans).toEqual([
+ expect.objectContaining({
+ description: 'otel span',
+ data: {
+ 'sentry.cloudflare_tracer': true,
+ 'sentry.origin': 'manual',
+ },
+ }),
+ ]);
+ });
+});
diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts
index 4fc9b308ec54..32bc8068ba6d 100644
--- a/packages/cloudflare/test/request.test.ts
+++ b/packages/cloudflare/test/request.test.ts
@@ -1,9 +1,10 @@
// Note: These tests run the handler in Node.js, which has some differences to the cloudflare workers runtime.
// Although this is not ideal, this is the best we can do until we have a better way to test cloudflare workers.
+import type { ExecutionContext } from '@cloudflare/workers-types';
import type { Event } from '@sentry/core';
import * as SentryCore from '@sentry/core';
-import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
+import { beforeAll, beforeEach, describe, expect, onTestFinished, test, vi } from 'vitest';
import { setAsyncLocalStorageAsyncContextStrategy } from '../src/async';
import type { CloudflareOptions } from '../src/client';
import { CloudflareClient } from '../src/client';
@@ -13,6 +14,10 @@ const MOCK_OPTIONS: CloudflareOptions = {
dsn: 'https://public@dsn.ingest.sentry.io/1337',
};
+function addDelayedWaitUntil(context: ExecutionContext) {
+ context.waitUntil(new Promise(resolve => setTimeout(() => resolve())));
+}
+
describe('withSentry', () => {
beforeAll(() => {
setAsyncLocalStorageAsyncContextStrategy();
@@ -33,15 +38,14 @@ describe('withSentry', () => {
test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => {
const context = createMockExecutionContext();
+ const waitUntilSpy = vi.spyOn(context, 'waitUntil');
await wrapRequestHandler(
{ options: MOCK_OPTIONS, request: new Request('https://example.com'), context },
() => new Response('test'),
);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenCalledTimes(1);
- // eslint-disable-next-line @typescript-eslint/unbound-method
- expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
+ expect(waitUntilSpy).toHaveBeenCalledTimes(1);
+ expect(waitUntilSpy).toHaveBeenLastCalledWith(expect.any(Promise));
});
test("doesn't error if context is undefined", () => {
@@ -64,6 +68,30 @@ describe('withSentry', () => {
expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object));
});
+ test('flush must be called when all waitUntil are done', async () => {
+ const flush = vi.spyOn(SentryCore.Client.prototype, 'flush');
+ vi.useFakeTimers();
+ onTestFinished(() => {
+ vi.useRealTimers();
+ });
+ const waits: Promise[] = [];
+ const waitUntil = vi.fn(promise => waits.push(promise));
+
+ const context = {
+ waitUntil,
+ } as unknown as ExecutionContext;
+
+ await wrapRequestHandler({ options: MOCK_OPTIONS, request: new Request('https://example.com'), context }, () => {
+ addDelayedWaitUntil(context);
+ return new Response('test');
+ });
+ expect(flush).not.toBeCalled();
+ expect(waitUntil).toBeCalled();
+ vi.advanceTimersToNextTimerAsync().then(() => vi.runAllTimers());
+ await Promise.all(waits);
+ expect(flush).toHaveBeenCalledOnce();
+ });
+
describe('scope instrumentation', () => {
test('adds cloud resource context', async () => {
let sentryEvent: Event = {};
diff --git a/packages/cloudflare/test/sdk.test.ts b/packages/cloudflare/test/sdk.test.ts
index 5c876812b035..2f4ec7844559 100644
--- a/packages/cloudflare/test/sdk.test.ts
+++ b/packages/cloudflare/test/sdk.test.ts
@@ -1,9 +1,14 @@
import * as SentryCore from '@sentry/core';
-import { describe, expect, test, vi } from 'vitest';
+import { beforeEach, describe, expect, test, vi } from 'vitest';
import { CloudflareClient } from '../src/client';
import { init } from '../src/sdk';
+import { resetSdk } from './testUtils';
describe('init', () => {
+ beforeEach(() => {
+ resetSdk();
+ });
+
test('should call initAndBind with the correct options', () => {
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind');
const client = init({});
diff --git a/packages/cloudflare/test/testUtils.ts b/packages/cloudflare/test/testUtils.ts
new file mode 100644
index 000000000000..8dcd3d43a4d9
--- /dev/null
+++ b/packages/cloudflare/test/testUtils.ts
@@ -0,0 +1,21 @@
+import { context, propagation, trace } from '@opentelemetry/api';
+import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
+
+function resetGlobals(): void {
+ getCurrentScope().clear();
+ getCurrentScope().setClient(undefined);
+ getIsolationScope().clear();
+ getGlobalScope().clear();
+}
+
+function cleanupOtel(): void {
+ // Disable all globally registered APIs
+ trace.disable();
+ context.disable();
+ propagation.disable();
+}
+
+export function resetSdk(): void {
+ resetGlobals();
+ cleanupOtel();
+}
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 3dd8bd66d023..7997bd3345a0 100644
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -498,7 +498,8 @@ export abstract class Client {
): void;
/**
- * Register a callback for whenever a span is ended.
+ * Register a callback for after a span is ended.
+ * NOTE: The span cannot be mutated anymore in this callback.
* Receives the span as argument.
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index b4f09d89f381..7551478c9c88 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -123,6 +123,7 @@ export { captureFeedback } from './feedback';
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 type { FeatureFlag } from './utils/featureFlags';
export {
diff --git a/packages/node/src/integrations/tracing/vercelai/ai_sdk_attributes.ts b/packages/core/src/utils/vercel-ai-attributes.ts
similarity index 100%
rename from packages/node/src/integrations/tracing/vercelai/ai_sdk_attributes.ts
rename to packages/core/src/utils/vercel-ai-attributes.ts
diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai.ts
new file mode 100644
index 000000000000..2a653addd805
--- /dev/null
+++ b/packages/core/src/utils/vercel-ai.ts
@@ -0,0 +1,221 @@
+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, SpanJSON, SpanOrigin } from '../types-hoist/span';
+import { spanToJSON } from './spanUtils';
+import {
+ AI_MODEL_ID_ATTRIBUTE,
+ AI_MODEL_PROVIDER_ATTRIBUTE,
+ AI_PROMPT_ATTRIBUTE,
+ AI_PROMPT_MESSAGES_ATTRIBUTE,
+ AI_PROMPT_TOOLS_ATTRIBUTE,
+ AI_RESPONSE_TEXT_ATTRIBUTE,
+ AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
+ AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
+ AI_TOOL_CALL_ID_ATTRIBUTE,
+ AI_TOOL_CALL_NAME_ATTRIBUTE,
+ AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
+ AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
+ GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
+ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
+} from './vercel-ai-attributes';
+
+function addOriginToSpan(span: Span, origin: SpanOrigin): void {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, origin);
+}
+
+/**
+ * Post-process spans emitted by the Vercel AI SDK.
+ * This is supposed to be used in `client.on('spanStart', ...)
+ */
+function onVercelAiSpanStart(span: Span): void {
+ const { data: attributes, description: name } = spanToJSON(span);
+
+ if (!name) {
+ return;
+ }
+
+ // Tool call spans
+ // https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
+ if (attributes[AI_TOOL_CALL_NAME_ATTRIBUTE] && attributes[AI_TOOL_CALL_ID_ATTRIBUTE] && name === 'ai.toolCall') {
+ processToolCallSpan(span, attributes);
+ return;
+ }
+
+ // The AI and Provider must be defined for generate, stream, and embed spans.
+ // The id of the model
+ const aiModelId = attributes[AI_MODEL_ID_ATTRIBUTE];
+ // the provider of the model
+ const aiModelProvider = attributes[AI_MODEL_PROVIDER_ATTRIBUTE];
+ if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) {
+ return;
+ }
+
+ processGenerateSpan(span, name, attributes);
+}
+
+const vercelAiEventProcessor = Object.assign(
+ (event: Event): Event => {
+ if (event.type === 'transaction' && event.spans) {
+ for (const span of event.spans) {
+ // this mutates spans in-place
+ processEndedVercelAiSpan(span);
+ }
+ }
+ return event;
+ },
+ { id: 'VercelAiEventProcessor' },
+);
+
+/**
+ * Post-process spans emitted by the Vercel AI SDK.
+ */
+function processEndedVercelAiSpan(span: SpanJSON): void {
+ const { data: attributes, origin } = span;
+
+ if (origin !== 'auto.vercelai.otel') {
+ return;
+ }
+
+ renameAttributeKey(attributes, AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE);
+ renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE);
+
+ if (
+ typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' &&
+ typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number'
+ ) {
+ attributes['gen_ai.usage.total_tokens'] =
+ attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] + attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE];
+ }
+
+ // Rename AI SDK attributes to standardized gen_ai attributes
+ renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages');
+ renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text');
+ renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls');
+ renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools');
+}
+
+/**
+ * Renames an attribute key in the provided attributes object if the old key exists.
+ * This function safely handles null and undefined values.
+ */
+function renameAttributeKey(attributes: Record, oldKey: string, newKey: string): void {
+ if (attributes[oldKey] != null) {
+ attributes[newKey] = attributes[oldKey];
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete attributes[oldKey];
+ }
+}
+
+function processToolCallSpan(span: Span, attributes: SpanAttributes): void {
+ addOriginToSpan(span, 'auto.vercelai.otel');
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.execute_tool');
+ span.setAttribute('gen_ai.tool.call.id', attributes[AI_TOOL_CALL_ID_ATTRIBUTE]);
+ span.setAttribute('gen_ai.tool.name', attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]);
+ span.updateName(`execute_tool ${attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]}`);
+}
+
+function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes): void {
+ addOriginToSpan(span, 'auto.vercelai.otel');
+
+ const nameWthoutAi = name.replace('ai.', '');
+ span.setAttribute('ai.pipeline.name', nameWthoutAi);
+ span.updateName(nameWthoutAi);
+
+ // If a Telemetry name is set and it is a pipeline span, use that as the operation name
+ const functionId = attributes[AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE];
+ if (functionId && typeof functionId === 'string' && name.split('.').length - 1 === 1) {
+ span.updateName(`${nameWthoutAi} ${functionId}`);
+ span.setAttribute('ai.pipeline.name', functionId);
+ }
+
+ if (attributes[AI_PROMPT_ATTRIBUTE]) {
+ span.setAttribute('gen_ai.prompt', attributes[AI_PROMPT_ATTRIBUTE]);
+ }
+ if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) {
+ span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]);
+ }
+ span.setAttribute('ai.streaming', name.includes('stream'));
+
+ // Generate Spans
+ if (name === 'ai.generateText') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
+ return;
+ }
+
+ if (name === 'ai.generateText.doGenerate') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.generate_text');
+ span.updateName(`generate_text ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
+ return;
+ }
+
+ if (name === 'ai.streamText') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
+ return;
+ }
+
+ if (name === 'ai.streamText.doStream') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.stream_text');
+ span.updateName(`stream_text ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
+ return;
+ }
+
+ if (name === 'ai.generateObject') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
+ return;
+ }
+
+ if (name === 'ai.generateObject.doGenerate') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.generate_object');
+ span.updateName(`generate_object ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
+ return;
+ }
+
+ if (name === 'ai.streamObject') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
+ return;
+ }
+
+ if (name === 'ai.streamObject.doStream') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.stream_object');
+ span.updateName(`stream_object ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
+ return;
+ }
+
+ if (name === 'ai.embed') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
+ return;
+ }
+
+ if (name === 'ai.embed.doEmbed') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.embed');
+ span.updateName(`embed ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
+ return;
+ }
+
+ if (name === 'ai.embedMany') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
+ return;
+ }
+
+ if (name === 'ai.embedMany.doEmbed') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.embed_many');
+ span.updateName(`embed_many ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
+ return;
+ }
+
+ if (name.startsWith('ai.stream')) {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run');
+ return;
+ }
+}
+
+/**
+ * Add event processors to the given client to process Vercel AI spans.
+ */
+export function addVercelAiProcessors(client: Client): void {
+ client.on('spanStart', onVercelAiSpanStart);
+ // Note: We cannot do this on `spanEnd`, because the span cannot be mutated anymore at this point
+ client.addEventProcessor(vercelAiEventProcessor);
+}
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index e9586a9bd820..f0bed369acee 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -99,6 +99,7 @@ export {
redisIntegration,
tediousIntegration,
postgresIntegration,
+ postgresJsIntegration,
prismaIntegration,
hapiIntegration,
setupHapiErrorHandler,
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 0315988bbfe7..9ab46ee4b98b 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -94,7 +94,7 @@
"devDependencies": {
"@types/resolve": "1.20.3",
"eslint-plugin-react": "^7.31.11",
- "next": "13.2.0"
+ "next": "13.5.9"
},
"peerDependencies": {
"next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0"
diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts
index c8eabd00d85c..88050713ec8c 100644
--- a/packages/nextjs/src/config/withSentryConfig.ts
+++ b/packages/nextjs/src/config/withSentryConfig.ts
@@ -20,8 +20,12 @@ let showedExperimentalBuildModeWarning = false;
// Packages we auto-instrument need to be external for instrumentation to work
// Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages
// Others we need to add ourselves
+//
+// NOTE: 'ai' (Vercel AI SDK) is intentionally NOT included in this list.
+// When externalized, Next.js doesn't properly handle the package's conditional exports,
+// specifically the "react-server" export condition. This causes client-side code to be
+// loaded in server components instead of the appropriate server-side functions.
export const DEFAULT_SERVER_EXTERNAL_PACKAGES = [
- 'ai',
'amqplib',
'connect',
'dataloader',
diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts
index c630d545061c..04b73ea4c83e 100644
--- a/packages/nextjs/src/index.types.ts
+++ b/packages/nextjs/src/index.types.ts
@@ -22,6 +22,9 @@ export declare function init(
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
+// Different implementation in server and worker
+export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration;
+
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index cf951c3db8b6..1c02da9fff2e 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -23,6 +23,7 @@ export { mysqlIntegration } from './integrations/tracing/mysql';
export { mysql2Integration } from './integrations/tracing/mysql2';
export { redisIntegration } from './integrations/tracing/redis';
export { postgresIntegration } from './integrations/tracing/postgres';
+export { postgresJsIntegration } from './integrations/tracing/postgresjs';
export { prismaIntegration } from './integrations/tracing/prisma';
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';
export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa';
diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts
index 13805e18d575..b514cb80d32e 100644
--- a/packages/node/src/integrations/tracing/fastify/index.ts
+++ b/packages/node/src/integrations/tracing/fastify/index.ts
@@ -57,9 +57,42 @@ const INTEGRATION_NAME_V3 = 'Fastify-V3';
export const instrumentFastifyV3 = generateInstrumentOnce(INTEGRATION_NAME_V3, () => new FastifyInstrumentationV3());
+function handleFastifyError(
+ this: {
+ diagnosticsChannelExists?: boolean;
+ },
+ error: Error,
+ request: FastifyRequest & { opentelemetry?: () => { span?: Span } },
+ reply: FastifyReply,
+ shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean,
+ handlerOrigin: 'diagnostics-channel' | 'onError-hook',
+): void {
+ // Diagnostics channel runs before the onError hook, so we can use it to check if the handler was already registered
+ if (handlerOrigin === 'diagnostics-channel') {
+ this.diagnosticsChannelExists = true;
+ }
+
+ if (this.diagnosticsChannelExists && handlerOrigin === 'onError-hook') {
+ DEBUG_BUILD &&
+ logger.warn(
+ 'Fastify error handler was already registered via diagnostics channel.',
+ 'You can safely remove `setupFastifyErrorHandler` call.',
+ );
+
+ // If the diagnostics channel already exists, we don't need to handle the error again
+ return;
+ }
+
+ if (shouldHandleError(error, request, reply)) {
+ captureException(error);
+ }
+}
+
export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => {
const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation();
const plugin = fastifyOtelInstrumentationInstance.plugin();
+ const options = fastifyOtelInstrumentationInstance.getConfig();
+ const shouldHandleError = (options as FastifyHandlerOptions)?.shouldHandleError || defaultShouldHandleError;
// This message handler works for Fastify versions 3, 4 and 5
diagnosticsChannel.subscribe('fastify.initialization', message => {
@@ -78,8 +111,20 @@ export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () =>
});
});
+ // This diagnostics channel only works on Fastify version 5
+ // For versions 3 and 4, we use `setupFastifyErrorHandler` instead
+ diagnosticsChannel.subscribe('tracing:fastify.request.handler:error', message => {
+ const { error, request, reply } = message as {
+ error: Error;
+ request: FastifyRequest & { opentelemetry?: () => { span?: Span } };
+ reply: FastifyReply;
+ };
+
+ handleFastifyError.call(handleFastifyError, error, request, reply, shouldHandleError, 'diagnostics-channel');
+ });
+
// Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation
- return fastifyOtelInstrumentationInstance as Instrumentation;
+ return fastifyOtelInstrumentationInstance as Instrumentation;
});
const _fastifyIntegration = (() => {
@@ -143,15 +188,11 @@ function defaultShouldHandleError(_error: Error, _request: FastifyRequest, reply
*/
export function setupFastifyErrorHandler(fastify: FastifyInstance, options?: Partial): void {
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
-
const plugin = Object.assign(
function (fastify: FastifyInstance, _options: unknown, done: () => void): void {
fastify.addHook('onError', async (request, reply, error) => {
- if (shouldHandleError(error, request, reply)) {
- captureException(error);
- }
+ handleFastifyError.call(handleFastifyError, error, request, reply, shouldHandleError, 'onError-hook');
});
-
done();
},
{
diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts
index 425710cae0ce..e7122562d619 100644
--- a/packages/node/src/integrations/tracing/index.ts
+++ b/packages/node/src/integrations/tracing/index.ts
@@ -15,6 +15,7 @@ import { instrumentMongoose, mongooseIntegration } from './mongoose';
import { instrumentMysql, mysqlIntegration } from './mysql';
import { instrumentMysql2, mysql2Integration } from './mysql2';
import { instrumentPostgres, postgresIntegration } from './postgres';
+import { instrumentPostgresJs, postgresJsIntegration } from './postgresjs';
import { prismaIntegration } from './prisma';
import { instrumentRedis, redisIntegration } from './redis';
import { instrumentTedious, tediousIntegration } from './tedious';
@@ -44,6 +45,7 @@ export function getAutoPerformanceIntegrations(): Integration[] {
amqplibIntegration(),
lruMemoizerIntegration(),
vercelAIIntegration(),
+ postgresJsIntegration(),
];
}
@@ -75,5 +77,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) =>
instrumentGenericPool,
instrumentAmqplib,
instrumentVercelAi,
+ instrumentPostgresJs,
];
}
diff --git a/packages/node/src/integrations/tracing/postgresjs.ts b/packages/node/src/integrations/tracing/postgresjs.ts
new file mode 100644
index 000000000000..c5efb7f6bef7
--- /dev/null
+++ b/packages/node/src/integrations/tracing/postgresjs.ts
@@ -0,0 +1,327 @@
+// Instrumentation for https://github.com/porsager/postgres
+import { context, trace } from '@opentelemetry/api';
+import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
+import {
+ InstrumentationBase,
+ InstrumentationNodeModuleDefinition,
+ InstrumentationNodeModuleFile,
+ safeExecuteInTheMiddle,
+} from '@opentelemetry/instrumentation';
+import {
+ ATTR_DB_NAMESPACE,
+ ATTR_DB_OPERATION_NAME,
+ ATTR_DB_QUERY_TEXT,
+ ATTR_DB_RESPONSE_STATUS_CODE,
+ ATTR_DB_SYSTEM_NAME,
+ ATTR_ERROR_TYPE,
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+} from '@opentelemetry/semantic-conventions';
+import type { IntegrationFn, Span } from '@sentry/core';
+import {
+ defineIntegration,
+ getCurrentScope,
+ logger,
+ SDK_VERSION,
+ SPAN_STATUS_ERROR,
+ startSpanManual,
+} from '@sentry/core';
+import { generateInstrumentOnce } from '../../otel/instrument';
+import { addOriginToSpan } from '../../utils/addOriginToSpan';
+
+const INTEGRATION_NAME = 'PostgresJs';
+const SUPPORTED_VERSIONS = ['>=3.0.0 <4'];
+
+type PostgresConnectionContext = {
+ ATTR_DB_NAMESPACE?: string; // Database name
+ ATTR_SERVER_ADDRESS?: string; // Hostname or IP address of the database server
+ ATTR_SERVER_PORT?: string; // Port number of the database server
+};
+
+type PostgresJsInstrumentationConfig = InstrumentationConfig & {
+ /**
+ * Whether to require a parent span for the instrumentation.
+ * If set to true, the instrumentation will only create spans if there is a parent span
+ * available in the current scope.
+ * @default true
+ */
+ requireParentSpan?: boolean;
+ /**
+ * Hook to modify the span before it is started.
+ * This can be used to set additional attributes or modify the span in any way.
+ */
+ requestHook?: (span: Span, sanitizedSqlQuery: string, postgresConnectionContext?: PostgresConnectionContext) => void;
+};
+
+export const instrumentPostgresJs = generateInstrumentOnce(
+ INTEGRATION_NAME,
+ (options?: PostgresJsInstrumentationConfig) =>
+ new PostgresJsInstrumentation({
+ requireParentSpan: options?.requireParentSpan ?? true,
+ requestHook: options?.requestHook,
+ }),
+);
+
+/**
+ * Instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library.
+ * This instrumentation captures postgresjs queries and their attributes,
+ */
+export class PostgresJsInstrumentation extends InstrumentationBase {
+ public constructor(config: PostgresJsInstrumentationConfig) {
+ super('sentry-postgres-js', SDK_VERSION, config);
+ }
+
+ /**
+ * Initializes the instrumentation.
+ */
+ public init(): InstrumentationNodeModuleDefinition[] {
+ const instrumentationModule = new InstrumentationNodeModuleDefinition('postgres', SUPPORTED_VERSIONS);
+
+ ['src', 'cf/src', 'cjs/src'].forEach(path => {
+ instrumentationModule.files.push(
+ new InstrumentationNodeModuleFile(
+ `postgres/${path}/connection.js`,
+ ['*'],
+ this._patchConnection.bind(this),
+ this._unwrap.bind(this),
+ ),
+ );
+
+ instrumentationModule.files.push(
+ new InstrumentationNodeModuleFile(
+ `postgres/${path}/query.js`,
+ SUPPORTED_VERSIONS,
+ this._patchQuery.bind(this),
+ this._unwrap.bind(this),
+ ),
+ );
+ });
+
+ return [instrumentationModule];
+ }
+
+ /**
+ * Determines whether a span should be created based on the current context.
+ * If `requireParentSpan` is set to true in the configuration, a span will
+ * only be created if there is a parent span available.
+ */
+ private _shouldCreateSpans(): boolean {
+ const config = this.getConfig();
+ const hasParentSpan = trace.getSpan(context.active()) !== undefined;
+ return hasParentSpan || !config.requireParentSpan;
+ }
+
+ /**
+ * Patches the reject method of the Query class to set the span status and end it
+ */
+ private _patchReject(rejectTarget: any, span: Span): any {
+ return new Proxy(rejectTarget, {
+ apply: (
+ rejectTarget,
+ rejectThisArg,
+ rejectArgs: {
+ message?: string;
+ code?: string;
+ name?: string;
+ }[],
+ ) => {
+ span.setStatus({
+ code: SPAN_STATUS_ERROR,
+ // This message is the error message from the rejectArgs, when available
+ // e.g "relation 'User' does not exist"
+ message: rejectArgs?.[0]?.message || 'unknown_error',
+ });
+
+ const result = Reflect.apply(rejectTarget, rejectThisArg, rejectArgs);
+
+ // This status code is PG error code, e.g. '42P01' for "relation does not exist"
+ // https://www.postgresql.org/docs/current/errcodes-appendix.html
+ span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'Unknown error');
+ // This is the error type, e.g. 'PostgresError' for a Postgres error
+ span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'Unknown error');
+
+ span.end();
+ return result;
+ },
+ });
+ }
+
+ /**
+ * Patches the resolve method of the Query class to end the span when the query is resolved.
+ */
+ private _patchResolve(resolveTarget: any, span: Span): any {
+ return new Proxy(resolveTarget, {
+ apply: (resolveTarget, resolveThisArg, resolveArgs: [{ command?: string }]) => {
+ const result = Reflect.apply(resolveTarget, resolveThisArg, resolveArgs);
+ const sqlCommand = resolveArgs?.[0]?.command;
+
+ if (sqlCommand) {
+ // SQL command is only available when the query is resolved successfully
+ span.setAttribute(ATTR_DB_OPERATION_NAME, sqlCommand);
+ }
+ span.end();
+ return result;
+ },
+ });
+ }
+
+ /**
+ * Patches the Query class to instrument the handle method.
+ */
+ private _patchQuery(moduleExports: {
+ Query: {
+ prototype: {
+ handle: any;
+ };
+ };
+ }): any {
+ moduleExports.Query.prototype.handle = new Proxy(moduleExports.Query.prototype.handle, {
+ apply: async (
+ handleTarget,
+ handleThisArg: {
+ resolve: any;
+ reject: any;
+ strings?: string[];
+ },
+ handleArgs,
+ ) => {
+ if (!this._shouldCreateSpans()) {
+ // If we don't need to create spans, just call the original method
+ return Reflect.apply(handleTarget, handleThisArg, handleArgs);
+ }
+
+ const sanitizedSqlQuery = this._sanitizeSqlQuery(handleThisArg.strings?.[0]);
+
+ return startSpanManual(
+ {
+ name: sanitizedSqlQuery || 'postgresjs.query',
+ op: 'db',
+ },
+ (span: Span) => {
+ const scope = getCurrentScope();
+ const postgresConnectionContext = scope.getScopeData().contexts['postgresjsConnection'] as
+ | PostgresConnectionContext
+ | undefined;
+
+ addOriginToSpan(span, 'auto.db.otel.postgres');
+
+ const { requestHook } = this.getConfig();
+
+ if (requestHook) {
+ safeExecuteInTheMiddle(
+ () => requestHook(span, sanitizedSqlQuery, postgresConnectionContext),
+ error => {
+ if (error) {
+ logger.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, error);
+ }
+ },
+ );
+ }
+
+ // ATTR_DB_NAMESPACE is used to indicate the database name and the schema name
+ // It's only the database name as we don't have the schema information
+ const databaseName = postgresConnectionContext?.ATTR_DB_NAMESPACE || '';
+ const databaseHost = postgresConnectionContext?.ATTR_SERVER_ADDRESS || '';
+ const databasePort = postgresConnectionContext?.ATTR_SERVER_PORT || '';
+
+ span.setAttribute(ATTR_DB_SYSTEM_NAME, 'postgres');
+ span.setAttribute(ATTR_DB_NAMESPACE, databaseName);
+ span.setAttribute(ATTR_SERVER_ADDRESS, databaseHost);
+ span.setAttribute(ATTR_SERVER_PORT, databasePort);
+ span.setAttribute(ATTR_DB_QUERY_TEXT, sanitizedSqlQuery);
+
+ handleThisArg.resolve = this._patchResolve(handleThisArg.resolve, span);
+ handleThisArg.reject = this._patchReject(handleThisArg.reject, span);
+
+ try {
+ return Reflect.apply(handleTarget, handleThisArg, handleArgs);
+ } catch (error) {
+ span.setStatus({
+ code: SPAN_STATUS_ERROR,
+ });
+ span.end();
+ throw error; // Re-throw the error to propagate it
+ }
+ },
+ );
+ },
+ });
+
+ return moduleExports;
+ }
+
+ /**
+ * Patches the Connection class to set the database, host, and port attributes
+ * when a new connection is created.
+ */
+ private _patchConnection(Connection: any): any {
+ return new Proxy(Connection, {
+ apply: (connectionTarget, thisArg, connectionArgs: { database: string; host: string[]; port: number[] }[]) => {
+ const databaseName = connectionArgs[0]?.database || '';
+ const databaseHost = connectionArgs[0]?.host?.[0] || '';
+ const databasePort = connectionArgs[0]?.port?.[0] || '';
+
+ const scope = getCurrentScope();
+ scope.setContext('postgresjsConnection', {
+ ATTR_DB_NAMESPACE: databaseName,
+ ATTR_SERVER_ADDRESS: databaseHost,
+ ATTR_SERVER_PORT: databasePort,
+ });
+
+ return Reflect.apply(connectionTarget, thisArg, connectionArgs);
+ },
+ });
+ }
+
+ /**
+ * Sanitize SQL query as per the OTEL semantic conventions
+ * https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sanitization-of-dbquerytext
+ */
+ private _sanitizeSqlQuery(sqlQuery: string | undefined): string {
+ if (!sqlQuery) {
+ return 'Unknown SQL Query';
+ }
+
+ return (
+ sqlQuery
+ .replace(/\s+/g, ' ')
+ .trim() // Remove extra spaces including newlines and trim
+ .substring(0, 1024) // Truncate to 1024 characters
+ .replace(/--.*?(\r?\n|$)/g, '') // Single line comments
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Multi-line comments
+ .replace(/;\s*$/, '') // Remove trailing semicolons
+ .replace(/\b\d+\b/g, '?') // Replace standalone numbers
+ // Collapse whitespace to a single space
+ .replace(/\s+/g, ' ')
+ // Collapse IN and in clauses
+ // eg. IN (?, ?, ?, ?) to IN (?)
+ .replace(/\bIN\b\s*\(\s*\?(?:\s*,\s*\?)*\s*\)/g, 'IN (?)')
+ );
+ }
+}
+
+const _postgresJsIntegration = (() => {
+ return {
+ name: INTEGRATION_NAME,
+ setupOnce() {
+ instrumentPostgresJs();
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Adds Sentry tracing instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library.
+ *
+ * For more information, see the [`postgresIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/postgres/).
+ *
+ * @example
+ * ```javascript
+ * const Sentry = require('@sentry/node');
+ *
+ * Sentry.init({
+ * integrations: [Sentry.postgresJsIntegration()],
+ * });
+ * ```
+ */
+
+export const postgresJsIntegration = defineIntegration(_postgresJsIntegration);
diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts
index 8ba6cb5af905..9ee5fb29f11d 100644
--- a/packages/node/src/integrations/tracing/vercelai/index.ts
+++ b/packages/node/src/integrations/tracing/vercelai/index.ts
@@ -1,27 +1,7 @@
-/* eslint-disable @typescript-eslint/no-dynamic-delete */
-/* eslint-disable complexity */
import type { Client, IntegrationFn } from '@sentry/core';
-import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core';
+import { addVercelAiProcessors, defineIntegration } from '@sentry/core';
import { generateInstrumentOnce } from '../../../otel/instrument';
-import { addOriginToSpan } from '../../../utils/addOriginToSpan';
import type { modulesIntegration } from '../../modules';
-import {
- AI_MODEL_ID_ATTRIBUTE,
- AI_MODEL_PROVIDER_ATTRIBUTE,
- AI_PROMPT_ATTRIBUTE,
- AI_PROMPT_MESSAGES_ATTRIBUTE,
- AI_PROMPT_TOOLS_ATTRIBUTE,
- AI_RESPONSE_TEXT_ATTRIBUTE,
- AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
- AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
- AI_TOOL_CALL_ID_ATTRIBUTE,
- AI_TOOL_CALL_NAME_ATTRIBUTE,
- AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
- AI_USAGE_PROMPT_TOKENS_ATTRIBUTE,
- GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
- GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
- GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
-} from './ai_sdk_attributes';
import { INTEGRATION_NAME } from './constants';
import { SentryVercelAiInstrumentation } from './instrumentation';
import type { VercelAiOptions } from './types';
@@ -47,175 +27,14 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
instrumentation = instrumentVercelAi();
},
afterAllSetup(client) {
- function registerProcessors(): void {
- client.on('spanStart', span => {
- const { data: attributes, description: name } = spanToJSON(span);
-
- if (!name) {
- return;
- }
-
- // Tool call spans
- // https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
- if (
- attributes[AI_TOOL_CALL_NAME_ATTRIBUTE] &&
- attributes[AI_TOOL_CALL_ID_ATTRIBUTE] &&
- name === 'ai.toolCall'
- ) {
- addOriginToSpan(span, 'auto.vercelai.otel');
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.execute_tool');
- span.setAttribute('gen_ai.tool.call.id', attributes[AI_TOOL_CALL_ID_ATTRIBUTE]);
- span.setAttribute('gen_ai.tool.name', attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]);
- span.updateName(`execute_tool ${attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]}`);
- return;
- }
-
- // The AI and Provider must be defined for generate, stream, and embed spans.
- // The id of the model
- const aiModelId = attributes[AI_MODEL_ID_ATTRIBUTE];
- // the provider of the model
- const aiModelProvider = attributes[AI_MODEL_PROVIDER_ATTRIBUTE];
- if (typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || !aiModelId || !aiModelProvider) {
- return;
- }
-
- addOriginToSpan(span, 'auto.vercelai.otel');
-
- const nameWthoutAi = name.replace('ai.', '');
- span.setAttribute('ai.pipeline.name', nameWthoutAi);
- span.updateName(nameWthoutAi);
-
- // If a Telemetry name is set and it is a pipeline span, use that as the operation name
- const functionId = attributes[AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE];
- if (functionId && typeof functionId === 'string' && name.split('.').length - 1 === 1) {
- span.updateName(`${nameWthoutAi} ${functionId}`);
- span.setAttribute('ai.pipeline.name', functionId);
- }
-
- if (attributes[AI_PROMPT_ATTRIBUTE]) {
- span.setAttribute('gen_ai.prompt', attributes[AI_PROMPT_ATTRIBUTE]);
- }
- if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) {
- span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]);
- }
- span.setAttribute('ai.streaming', name.includes('stream'));
-
- // Generate Spans
- if (name === 'ai.generateText') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
- return;
- }
-
- if (name === 'ai.generateText.doGenerate') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.generate_text');
- span.updateName(`generate_text ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
- return;
- }
-
- if (name === 'ai.streamText') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
- return;
- }
-
- if (name === 'ai.streamText.doStream') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.stream_text');
- span.updateName(`stream_text ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
- return;
- }
-
- if (name === 'ai.generateObject') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
- return;
- }
-
- if (name === 'ai.generateObject.doGenerate') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.generate_object');
- span.updateName(`generate_object ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
- return;
- }
-
- if (name === 'ai.streamObject') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
- return;
- }
-
- if (name === 'ai.streamObject.doStream') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.stream_object');
- span.updateName(`stream_object ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
- return;
- }
-
- if (name === 'ai.embed') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
- return;
- }
-
- if (name === 'ai.embed.doEmbed') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.embed');
- span.updateName(`embed ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
- return;
- }
-
- if (name === 'ai.embedMany') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.invoke_agent');
- return;
- }
-
- if (name === 'ai.embedMany.doEmbed') {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.embed_many');
- span.updateName(`embed_many ${attributes[AI_MODEL_ID_ATTRIBUTE]}`);
- return;
- }
-
- if (name.startsWith('ai.stream')) {
- span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run');
- return;
- }
- });
-
- client.addEventProcessor(event => {
- if (event.type === 'transaction' && event.spans?.length) {
- for (const span of event.spans) {
- const { data: attributes, description: name } = span;
-
- if (!name || span.origin !== 'auto.vercelai.otel') {
- continue;
- }
-
- renameAttributeKey(
- attributes,
- AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE,
- GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
- );
- renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE);
- if (
- typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' &&
- typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number'
- ) {
- attributes['gen_ai.usage.total_tokens'] =
- attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] + attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE];
- }
-
- // Rename AI SDK attributes to standardized gen_ai attributes
- renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages');
- renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text');
- renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls');
- renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools');
- }
- }
-
- return event;
- });
- }
-
// Auto-detect if we should force the integration when running with 'ai' package available
// Note that this can only be detected if the 'Modules' integration is available, and running in CJS mode
const shouldForce = options.force ?? shouldForceIntegration(client);
if (shouldForce) {
- registerProcessors();
+ addVercelAiProcessors(client);
} else {
- instrumentation?.callWhenPatched(registerProcessors);
+ instrumentation?.callWhenPatched(() => addVercelAiProcessors(client));
}
},
};
@@ -223,6 +42,7 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
/**
* Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library.
+ * This integration is not enabled by default, you need to manually add it.
*
* For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry).
*
@@ -235,17 +55,14 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
* });
* ```
*
- * The integration automatically detects when to force registration in CommonJS environments
- * when the 'ai' package is available. You can still manually set the `force` option if needed.
- *
- * By default this integration adds tracing support to all `ai` function calls. If you need to disable
- * collecting spans for a specific call, you can do so by setting `experimental_telemetry.isEnabled` to
- * `false` in the first argument of the function call.
+ * This integration adds tracing support to all `ai` function calls.
+ * You need to opt-in to collecting spans for a specific call,
+ * you can do so by setting `experimental_telemetry.isEnabled` to `true` in the first argument of the function call.
*
* ```javascript
* const result = await generateText({
* model: openai('gpt-4-turbo'),
- * experimental_telemetry: { isEnabled: false },
+ * experimental_telemetry: { isEnabled: true },
* });
* ```
*
@@ -260,14 +77,3 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
* });
*/
export const vercelAIIntegration = defineIntegration(_vercelAIIntegration);
-
-/**
- * Renames an attribute key in the provided attributes object if the old key exists.
- * This function safely handles null and undefined values.
- */
-function renameAttributeKey(attributes: Record, oldKey: string, newKey: string): void {
- if (attributes[oldKey] != null) {
- attributes[newKey] = attributes[oldKey];
- delete attributes[oldKey];
- }
-}
diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts
index f11b9bb51077..9574b38f1e47 100644
--- a/packages/solidstart/src/server/index.ts
+++ b/packages/solidstart/src/server/index.ts
@@ -75,6 +75,7 @@ export {
onUnhandledRejectionIntegration,
parameterize,
postgresIntegration,
+ postgresJsIntegration,
prismaIntegration,
redisIntegration,
requestDataIntegration,
diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts
index 03c63041e726..df788559d2f5 100644
--- a/packages/sveltekit/src/index.types.ts
+++ b/packages/sveltekit/src/index.types.ts
@@ -46,6 +46,9 @@ export declare function wrapLoadWithSentry any>(orig
export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration;
export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration;
+// Different implementation in server and worker
+export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration;
+
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 717dd7387c98..07d92d03c8ce 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -77,6 +77,7 @@ export {
onUnhandledRejectionIntegration,
parameterize,
postgresIntegration,
+ postgresJsIntegration,
prismaIntegration,
redisIntegration,
requestDataIntegration,
diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts
index e49a493fb0b8..8e4645741456 100644
--- a/packages/sveltekit/src/worker/index.ts
+++ b/packages/sveltekit/src/worker/index.ts
@@ -84,6 +84,7 @@ export {
instrumentSupabaseClient,
zodErrorsIntegration,
featureFlagsIntegration,
+ vercelAIIntegration,
type FeatureFlagsIntegration,
} from '@sentry/cloudflare';
diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts
index 303d40144ec3..5325d1e62391 100644
--- a/packages/vercel-edge/src/index.ts
+++ b/packages/vercel-edge/src/index.ts
@@ -98,5 +98,6 @@ export { VercelEdgeClient } from './client';
export { getDefaultIntegrations, init } from './sdk';
export { winterCGFetchIntegration } from './integrations/wintercg-fetch';
+export { vercelAIIntegration } from './integrations/tracing/vercelai';
export * as logger from './logs/exports';
diff --git a/packages/vercel-edge/src/integrations/tracing/vercelai.ts b/packages/vercel-edge/src/integrations/tracing/vercelai.ts
new file mode 100644
index 000000000000..c513568997ab
--- /dev/null
+++ b/packages/vercel-edge/src/integrations/tracing/vercelai.ts
@@ -0,0 +1,51 @@
+/**
+ * This is a copy of the Vercel AI integration from the node SDK.
+ *
+ * The only difference is that it does not use `@opentelemetry/instrumentation`
+ * because Cloudflare Workers do not support it.
+ *
+ * Therefore, we cannot automatically patch setting `experimental_telemetry: { isEnabled: true }`
+ * and users have to manually set this to get spans.
+ */
+
+import type { IntegrationFn } from '@sentry/core';
+import { addVercelAiProcessors, defineIntegration } from '@sentry/core';
+
+const INTEGRATION_NAME = 'VercelAI';
+
+const _vercelAIIntegration = (() => {
+ return {
+ name: INTEGRATION_NAME,
+ setup(client) {
+ addVercelAiProcessors(client);
+ },
+ };
+}) satisfies IntegrationFn;
+
+/**
+ * Adds Sentry tracing instrumentation for the [ai](https://www.npmjs.com/package/ai) library.
+ * This integration is not enabled by default, you need to manually add it.
+ *
+ * For more information, see the [`ai` documentation](https://sdk.vercel.ai/docs/ai-sdk-core/telemetry).
+ *
+ * You need to enable collecting spans for a specific call by setting
+ * `experimental_telemetry.isEnabled` to `true` in the first argument of the function call.
+ *
+ * ```javascript
+ * const result = await generateText({
+ * model: openai('gpt-4-turbo'),
+ * experimental_telemetry: { isEnabled: true },
+ * });
+ * ```
+ *
+ * If you want to collect inputs and outputs for a specific call, you must specifically opt-in to each
+ * function call by setting `experimental_telemetry.recordInputs` and `experimental_telemetry.recordOutputs`
+ * to `true`.
+ *
+ * ```javascript
+ * const result = await generateText({
+ * model: openai('gpt-4-turbo'),
+ * experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true },
+ * });
+ */
+export const vercelAIIntegration = defineIntegration(_vercelAIIntegration);
diff --git a/yarn.lock b/yarn.lock
index f696182ab447..ac3970cfb153 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4676,75 +4676,55 @@
"@netlify/node-cookies" "^0.1.0"
urlpattern-polyfill "8.0.2"
-"@next/env@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-13.2.0.tgz#1a597a885ce11860446c88e1098fd517dc0e84b1"
- integrity sha512-yv9oaRVa+AxFa27uQOVecS931NrE+GcQSqcL2HaRxL8NunStLtPiyNm/VixvdzfiWLabMz4dXvbXfwCNaECzcw==
-
-"@next/swc-android-arm-eabi@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.0.tgz#241d007fdb2f06f70ab21d5a333e08a29de9cd7f"
- integrity sha512-VMetUwBWtDBGzNiOkhiWTP+99ZYW5NVRpIGlUsldEtY8IQIqleaUgW9iamsO0kDSjhWNdCQCB+xu5HcCvmDTww==
-
-"@next/swc-android-arm64@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.0.tgz#924e79197f094a12ac3409b6a416f84c2f74341d"
- integrity sha512-fAiP54Om3fSj5aKntxEvW5fWzyMUzLzjFrHuUt5jBnTRWM4QikhLy547OZDoxycyk4GoQVHmNMSA3hILsrV/dQ==
-
-"@next/swc-darwin-arm64@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.0.tgz#bfd3dfe90903b3bbf81f617f2b1d4f8b9e20e8aa"
- integrity sha512-F4zbvPnq3zCTqyyM6WN8ledazzJx3OrxIdc2ewnqnfk6tjBZ/aq1M27GhEfylGjZG1KvbtJCxUqi7dR/6R94bA==
-
-"@next/swc-darwin-x64@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.0.tgz#3d159bb2889f093d173546a6e5edd6260df7e156"
- integrity sha512-Y9+fB7TLAAnkCZQXWjwJg5bi1pT5NuNkI+HoKYp26U1J0SxW5vZWFGc31WFmmHIz3wA0zlaQfRa4mF7cpZL5yw==
-
-"@next/swc-freebsd-x64@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.0.tgz#33877ad933e1b3d7776dfb060f4e3b55f675523e"
- integrity sha512-b9bCLlfznbV6e6Vg9wKYZJs7Uz8z/Py9105MYq95a3JlHiI3e/fvBpm1c7fe5QlvWJlqyNav6Clyu1W+lDk+IQ==
-
-"@next/swc-linux-arm-gnueabihf@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.0.tgz#31b81ff368e5337019f858c4a0d7ae7f80310728"
- integrity sha512-jY/2JjDVVyktzRtMclAIVLgOxk5Ut9NKu8kKMCPdKMf9/ila37UpRfIh2fOXtRhv8AK7Lq/iSI/v2vjopZxZgQ==
-
-"@next/swc-linux-arm64-gnu@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.0.tgz#e46bd73d1ccc38be46009b88dc87df88d8c44fa1"
- integrity sha512-EKjWU3/lSBhOwPQRQLbySUnATnXygCjGd8ag3rP6d7kTIhfuPO4pY+DYW+wHOt5qB1ULNRmW0sXZ/ZKnQrVszw==
-
-"@next/swc-linux-arm64-musl@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.0.tgz#54a4f0cc1431f3e4e24a5e017e473a5fc761b33b"
- integrity sha512-T5R9r23Docwo6PYZRzndeFB5WUN3+smMbyk25K50MAngCiSydr82/YfAetcp7Ov7Shp4a8xXP9DHDIsBas6wbQ==
-
-"@next/swc-linux-x64-gnu@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.0.tgz#1c17a6121846decac2d701a040649f8f764ac671"
- integrity sha512-FeXTc2KFvUSnTJmkpNMKoBHmNA1Ujr3QdfcKnVm/gXWqK+rfuEhAiRNOo+6mPcQ0noEge1j8Ai+W1LTbdDwPZQ==
-
-"@next/swc-linux-x64-musl@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.0.tgz#a4f39cfeb19123196a2ebc2061e60897b29ffa3f"
- integrity sha512-7Y0XMUzWDWI94pxC0xWGMWrgTFKHu/myc+GTNVEwvLtI9WA0brKqZrL1tCQW/+t6J+5XqS7w+AHbViaF+muu1A==
-
-"@next/swc-win32-arm64-msvc@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.0.tgz#d21ef0d0c2757cee9b1d723aafb9e02ca25852eb"
- integrity sha512-NM5h2gEMe8EtvOeRU3vRM83tq1xo6Qvhuz0xJem/176SAMxbqzAz4LLP3l9VyUI3SIzGyiztvF/1c0jqeq7UEA==
-
-"@next/swc-win32-ia32-msvc@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.0.tgz#529852721e3f00afb9385640cbac1a899342c6ba"
- integrity sha512-G7YEJZX9wkcUaBOvXQSCF9Wb2sqP8hhsmFXF6po7M3llw4b+2ut2DXLf+UMdthOdUK0u+Ijhy5F7SbW9HOn2ig==
-
-"@next/swc-win32-x64-msvc@13.2.0":
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.0.tgz#49bc50b1865d20b29892a415fcbaab84217112a5"
- integrity sha512-QTAjSuPevnZnlHfC4600+4NvxRuPar6tWdYbPum9vnk3OIH1xu9YLK+2ArPGFd0bB2K8AoY2SIMbs1dhK0GjQQ==
+"@next/env@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.9.tgz#3298c57c9ad9f333774484e03cf20fba90cd79c4"
+ integrity sha512-h9+DconfsLkhHIw950Som5t5DC0kZReRRVhT4XO2DLo5vBK3PQK6CbFr8unxjHwvIcRdDvb8rosKleLdirfShQ==
+
+"@next/swc-darwin-arm64@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.9.tgz#46c3a525039171ff1a83c813d7db86fb7808a9b2"
+ integrity sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==
+
+"@next/swc-darwin-x64@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.9.tgz#b690452e9a6ce839f8738e27e9fd1a8567dd7554"
+ integrity sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==
+
+"@next/swc-linux-arm64-gnu@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.9.tgz#c3e335e2da3ba932c0b2f571f0672d1aa7af33df"
+ integrity sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==
+
+"@next/swc-linux-arm64-musl@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.9.tgz#54600d4917bace2508725cc963eeeb3b6432889e"
+ integrity sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==
+
+"@next/swc-linux-x64-gnu@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.9.tgz#f869c2066f13ff2818140e0a145dfea1ea7c0333"
+ integrity sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==
+
+"@next/swc-linux-x64-musl@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.9.tgz#09295ea60a42a1b22d927802d6e543d8a8bbb186"
+ integrity sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==
+
+"@next/swc-win32-arm64-msvc@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.9.tgz#f39e3513058d7af6e9f6b1f296bf071301217159"
+ integrity sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==
+
+"@next/swc-win32-ia32-msvc@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.9.tgz#d567f471e182efa4ea29f47f3030613dd3fc68b5"
+ integrity sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==
+
+"@next/swc-win32-x64-msvc@13.5.9":
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.9.tgz#35c53bd6d33040ec0ce1dd613c59112aac06b235"
+ integrity sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==
"@ngtools/webpack@14.2.13":
version "14.2.13"
@@ -7409,10 +7389,10 @@
svelte-hmr "^0.15.1"
vitefu "^0.2.2"
-"@swc/helpers@0.4.14":
- version "0.4.14"
- resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
- integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
+"@swc/helpers@0.5.2":
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
+ integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
dependencies:
tslib "^2.4.0"
@@ -11459,7 +11439,7 @@ bundle-name@^4.1.0:
dependencies:
run-applescript "^7.0.0"
-busboy@^1.0.0:
+busboy@1.6.0, busboy@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
@@ -21564,30 +21544,28 @@ new-find-package-json@^2.0.0:
dependencies:
debug "^4.3.4"
-next@13.2.0:
- version "13.2.0"
- resolved "https://registry.yarnpkg.com/next/-/next-13.2.0.tgz#100b2d1dca120a3460c767ccdad80fc8e2463e31"
- integrity sha512-vhByvKHedsaMwNTwXKzK4IMmNp7XI7vN4etcGUoIpLrIuDfoYA3bS0ImS4X9F6lKzopG5aVp7a1CjuzF2NGkvA==
+next@13.5.9:
+ version "13.5.9"
+ resolved "https://registry.yarnpkg.com/next/-/next-13.5.9.tgz#a8c38254279eb30a264c1c640bf77340289ba6e3"
+ integrity sha512-h4ciD/Uxf1PwsiX0DQePCS5rMoyU5a7rQ3/Pg6HBLwpa/SefgNj1QqKSZsWluBrYyqdtEyqKrjeOszgqZlyzFQ==
dependencies:
- "@next/env" "13.2.0"
- "@swc/helpers" "0.4.14"
+ "@next/env" "13.5.9"
+ "@swc/helpers" "0.5.2"
+ busboy "1.6.0"
caniuse-lite "^1.0.30001406"
- postcss "8.4.14"
+ postcss "8.4.31"
styled-jsx "5.1.1"
+ watchpack "2.4.0"
optionalDependencies:
- "@next/swc-android-arm-eabi" "13.2.0"
- "@next/swc-android-arm64" "13.2.0"
- "@next/swc-darwin-arm64" "13.2.0"
- "@next/swc-darwin-x64" "13.2.0"
- "@next/swc-freebsd-x64" "13.2.0"
- "@next/swc-linux-arm-gnueabihf" "13.2.0"
- "@next/swc-linux-arm64-gnu" "13.2.0"
- "@next/swc-linux-arm64-musl" "13.2.0"
- "@next/swc-linux-x64-gnu" "13.2.0"
- "@next/swc-linux-x64-musl" "13.2.0"
- "@next/swc-win32-arm64-msvc" "13.2.0"
- "@next/swc-win32-ia32-msvc" "13.2.0"
- "@next/swc-win32-x64-msvc" "13.2.0"
+ "@next/swc-darwin-arm64" "13.5.9"
+ "@next/swc-darwin-x64" "13.5.9"
+ "@next/swc-linux-arm64-gnu" "13.5.9"
+ "@next/swc-linux-arm64-musl" "13.5.9"
+ "@next/swc-linux-x64-gnu" "13.5.9"
+ "@next/swc-linux-x64-musl" "13.5.9"
+ "@next/swc-win32-arm64-msvc" "13.5.9"
+ "@next/swc-win32-ia32-msvc" "13.5.9"
+ "@next/swc-win32-x64-msvc" "13.5.9"
ng-packagr@^14.2.2:
version "14.3.0"
@@ -24120,15 +24098,6 @@ postcss-values-parser@^6.0.2:
is-url-superb "^4.0.0"
quote-unquote "^1.0.0"
-postcss@8.4.14:
- version "8.4.14"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
- integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
- dependencies:
- nanoid "^3.3.4"
- picocolors "^1.0.0"
- source-map-js "^1.0.2"
-
postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
@@ -24196,6 +24165,11 @@ postgres-range@^1.1.1:
resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76"
integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==
+postgres@^3.4.7:
+ version "3.4.7"
+ resolved "https://registry.yarnpkg.com/postgres/-/postgres-3.4.7.tgz#122f460a808fe300cae53f592108b9906e625345"
+ integrity sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==
+
preact@^10.19.4:
version "10.19.4"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.4.tgz#735d331d5b1bd2182cc36f2ba481fd6f0da3fe3b"
@@ -24560,16 +24534,6 @@ quick-format-unescaped@^4.0.3:
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
-quick-format-unescaped@^4.0.3:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
- integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
-
-quick-format-unescaped@^4.0.3:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
- integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
-
quick-lru@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
@@ -29485,6 +29449,14 @@ watch-detector@^1.0.0, watch-detector@^1.0.2:
silent-error "^1.1.1"
tmp "^0.1.0"
+watchpack@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
+ integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
+ dependencies:
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.1.2"
+
watchpack@^2.4.0, watchpack@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff"