diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 64709dd1f999..8acac6fd2709 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -51,7 +51,6 @@ body:
- '@sentry/nestjs'
- '@sentry/nextjs'
- '@sentry/nuxt'
- - '@sentry/pino-transport'
- '@sentry/react'
- '@sentry/react-router'
- '@sentry/remix'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a1100ba7ad0..1ae0eba65cec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,44 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 10.18.0
+
+### Important Changes
+
+- **feat(node): `pino` integration ([#17584](https://github.com/getsentry/sentry-javascript/pull/17584))**
+
+ This release adds a new `pino` integration for Node.js, enabling Sentry to capture logs from the Pino logging library.
+
+- **feat: Remove @sentry/pino-transport package ([#17851](https://github.com/getsentry/sentry-javascript/pull/17851))**
+
+ The `@sentry/pino-transport` package has been removed. Please use the new `pino` integration in `@sentry/node` instead.
+
+- **feat(node-core): Extend onnhandledrejection with ignore errors option ([#17736](https://github.com/getsentry/sentry-javascript/pull/17736))**
+
+ Added support for selectively suppressing specific errors with configurable logging control in onnhandledrejection integration.
+
+### Other Changes
+
+- feat(core): Rename vercelai.schema to gen_ai.request.schema ([#17850](https://github.com/getsentry/sentry-javascript/pull/17850))
+- feat(core): Support stream responses and tool calls for Google GenAI ([#17664](https://github.com/getsentry/sentry-javascript/pull/17664))
+- feat(nextjs): Attach headers using client hook ([#17831](https://github.com/getsentry/sentry-javascript/pull/17831))
+- fix(core): Keep all property values in baggage header ([#17847](https://github.com/getsentry/sentry-javascript/pull/17847))
+- fix(nestjs): Add support for Symbol as event name ([#17785](https://github.com/getsentry/sentry-javascript/pull/17785))
+- fix(nuxt): include `sentry.client.config.ts` in nuxt app types ([#17830](https://github.com/getsentry/sentry-javascript/pull/17830))
+- fix(react-router): Fix type for `OriginalHandleRequest` with middleware ([#17870](https://github.com/getsentry/sentry-javascript/pull/17870))
+
+
+ Internal Changes
+
+- chore: Add external contributor to CHANGELOG.md ([#17866](https://github.com/getsentry/sentry-javascript/pull/17866))
+- chore(deps): Bump @sentry/cli from 2.53.0 to 2.56.0 ([#17819](https://github.com/getsentry/sentry-javascript/pull/17819))
+- chore(deps): Bump axios in browser integration tests ([#17839](https://github.com/getsentry/sentry-javascript/pull/17839))
+- chore(deps): Bump nestjs in integration tests ([#17840](https://github.com/getsentry/sentry-javascript/pull/17840))
+
+
+
+Work in this release was contributed by @stefanvanderwolf. Thank you for your contribution!
+
## 10.17.0
### Important Changes
diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json
index 969f4bbb1e46..2ac5163bc855 100644
--- a/dev-packages/browser-integration-tests/package.json
+++ b/dev-packages/browser-integration-tests/package.json
@@ -45,7 +45,7 @@
"@sentry-internal/rrweb": "2.34.0",
"@sentry/browser": "10.17.0",
"@supabase/supabase-js": "2.49.3",
- "axios": "1.8.2",
+ "axios": "^1.12.2",
"babel-loader": "^8.2.2",
"fflate": "0.8.2",
"html-webpack-plugin": "^5.5.0",
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js
new file mode 100644
index 000000000000..76fafc9df148
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/subject.js
@@ -0,0 +1,9 @@
+fetchButton.addEventListener('click', () => {
+ // W3C spec example: property values can contain = signs
+ // See: https://www.w3.org/TR/baggage/#example
+ fetch('http://sentry-test-site.example/fetch-test', {
+ headers: {
+ baggage: 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue',
+ },
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html
new file mode 100644
index 000000000000..404eee952355
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/template.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Fetch Baggage Property Values Test
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts
new file mode 100644
index 000000000000..c191304ec8e0
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-baggage-property-values/test.ts
@@ -0,0 +1,41 @@
+import { expect } from '@playwright/test';
+import { TRACEPARENT_REGEXP } from '@sentry/core';
+import { sentryTest } from '../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest(
+ 'preserves baggage property values with equal signs in fetch requests',
+ async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const requestPromise = page.waitForRequest('http://sentry-test-site.example/fetch-test');
+
+ await page.goto(url);
+ await page.click('#fetchButton');
+
+ const request = await requestPromise;
+
+ const requestHeaders = request.headers();
+
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP),
+ });
+
+ const baggageHeader = requestHeaders.baggage;
+ expect(baggageHeader).toBeDefined();
+
+ const baggageItems = baggageHeader.split(',').map(item => decodeURIComponent(item.trim()));
+
+ // Verify property values with = signs are preserved
+ expect(baggageItems).toContainEqual(expect.stringContaining('key1=value1;property1;property2'));
+ expect(baggageItems).toContainEqual(expect.stringContaining('key2=value2'));
+ expect(baggageItems).toContainEqual(expect.stringContaining('key3=value3; propertyKey=propertyValue'));
+
+ // Verify Sentry baggage is also present
+ expect(baggageHeader).toMatch(/sentry-/);
+ },
+);
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js
new file mode 100644
index 000000000000..839cdf137fd7
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/subject.js
@@ -0,0 +1,8 @@
+const xhr = new XMLHttpRequest();
+
+xhr.open('GET', 'http://sentry-test-site.example/1');
+// W3C spec example: property values can contain = signs
+// See: https://www.w3.org/TR/baggage/#example
+xhr.setRequestHeader('baggage', 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue');
+
+xhr.send();
diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts
new file mode 100644
index 000000000000..f2ac4edb4a67
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-baggage-property-values/test.ts
@@ -0,0 +1,36 @@
+import { expect } from '@playwright/test';
+import { TRACEPARENT_REGEXP } from '@sentry/core';
+import { sentryTest } from '../../../../utils/fixtures';
+import { shouldSkipTracingTest } from '../../../../utils/helpers';
+
+sentryTest('preserves baggage property values with equal signs in XHR requests', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ const url = await getLocalTestUrl({ testDir: __dirname });
+
+ const requestPromise = page.waitForRequest('http://sentry-test-site.example/1');
+
+ await page.goto(url);
+
+ const request = await requestPromise;
+
+ const requestHeaders = request.headers();
+
+ expect(requestHeaders).toMatchObject({
+ 'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP),
+ });
+
+ const baggageHeader = requestHeaders.baggage;
+ expect(baggageHeader).toBeDefined();
+ const baggageItems = baggageHeader.split(',').map(item => decodeURIComponent(item.trim()));
+
+ // Verify property values with = signs are preserved
+ expect(baggageItems).toContainEqual(expect.stringContaining('key1=value1;property1;property2'));
+ expect(baggageItems).toContainEqual(expect.stringContaining('key2=value2'));
+ expect(baggageItems).toContainEqual(expect.stringContaining('key3=value3; propertyKey=propertyValue'));
+
+ // Verify Sentry baggage is also present
+ expect(baggageHeader).toMatch(/sentry-/);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts
new file mode 100644
index 000000000000..7cd1fc7e332c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/edge/route.ts
@@ -0,0 +1,8 @@
+import { NextResponse } from 'next/server';
+
+export const runtime = 'edge';
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ return NextResponse.json({ message: 'Hello Edge Route Handler' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts
new file mode 100644
index 000000000000..5bc418f077aa
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/route-handler/[xoxo]/node/route.ts
@@ -0,0 +1,7 @@
+import { NextResponse } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ return NextResponse.json({ message: 'Hello Node Route Handler' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
index 3cad4a546508..c9a9fc44bdba 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
@@ -24,11 +24,6 @@ test('App router transactions should be attached to the pageload request span',
});
test('extracts HTTP request headers as span attributes', async ({ baseURL }) => {
- test.skip(
- process.env.TEST_ENV === 'prod-turbopack' || process.env.TEST_ENV === 'dev-turbopack',
- 'Incoming fetch request headers are not added as span attributes when Turbopack is enabled (addHeadersAsAttributes)',
- );
-
const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === 'GET /pageload-tracing';
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts
new file mode 100644
index 000000000000..f9dedccb4923
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/route-handler.test.ts
@@ -0,0 +1,40 @@
+import test, { expect } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Should create a transaction for node route handlers', async ({ request }) => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ console.log(transactionEvent?.transaction);
+ return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/node';
+ });
+
+ const response = await request.get('/route-handler/123/node', { headers: { 'x-charly': 'gomez' } });
+ expect(await response.json()).toStrictEqual({ message: 'Hello Node Route Handler' });
+
+ const routehandlerTransaction = await routehandlerTransactionPromise;
+
+ expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
+ expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
+
+ // This is flaking on dev mode
+ if (process.env.TEST_ENV !== 'development' && process.env.TEST_ENV !== 'dev-turbopack') {
+ expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez');
+ }
+});
+
+test('Should create a transaction for edge route handlers', async ({ request }) => {
+ // This test only works for webpack builds on non-async param extraction
+ // todo: check if we can set request headers for edge on sdkProcessingMetadata
+ test.skip();
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/edge';
+ });
+
+ const response = await request.get('/route-handler/123/edge', { headers: { 'x-charly': 'gomez' } });
+ expect(await response.json()).toStrictEqual({ message: 'Hello Edge Route Handler' });
+
+ const routehandlerTransaction = await routehandlerTransactionPromise;
+
+ expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
+ expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
+ expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez');
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
index 596109c0a596..17c6f714c499 100644
--- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
+++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts
@@ -53,6 +53,7 @@ const DEPENDENTS: Dependent[] = [
'NODE_VERSION',
'childProcessIntegration',
'systemErrorIntegration',
+ 'pinoIntegration',
],
},
{
diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml
index 1d565dbecda2..0773603b033e 100644
--- a/dev-packages/e2e-tests/verdaccio-config/config.yaml
+++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml
@@ -122,12 +122,6 @@ packages:
unpublish: $all
# proxy: npmjs # Don't proxy for E2E tests!
- '@sentry/pino-transport':
- access: $all
- publish: $all
- unpublish: $all
- # proxy: npmjs # Don't proxy for E2E tests!
-
'@sentry/profiling-node':
access: $all
publish: $all
diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json
index eb6161cb9a80..5cec961c736a 100644
--- a/dev-packages/node-core-integration-tests/package.json
+++ b/dev-packages/node-core-integration-tests/package.json
@@ -23,9 +23,9 @@
"test:watch": "yarn test --watch"
},
"dependencies": {
- "@nestjs/common": "11.0.16",
- "@nestjs/core": "10.4.6",
- "@nestjs/platform-express": "10.4.6",
+ "@nestjs/common": "^11",
+ "@nestjs/core": "^11",
+ "@nestjs/platform-express": "^11",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.1.0",
"@opentelemetry/core": "^2.1.0",
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index dbf8d9001ed4..e7e811b40d14 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -28,9 +28,9 @@
"@google/genai": "^1.20.0",
"@hapi/hapi": "^21.3.10",
"@hono/node-server": "^1.19.4",
- "@nestjs/common": "11.1.3",
- "@nestjs/core": "11.1.3",
- "@nestjs/platform-express": "11.1.3",
+ "@nestjs/common": "^11",
+ "@nestjs/core": "^11",
+ "@nestjs/platform-express": "^11",
"@prisma/client": "6.15.0",
"@sentry/aws-serverless": "10.17.0",
"@sentry/core": "10.17.0",
@@ -66,6 +66,8 @@
"node-schedule": "^2.1.1",
"openai": "5.18.1",
"pg": "8.16.0",
+ "pino": "9.9.4",
+ "pino-next": "npm:pino@^9.12.0",
"postgres": "^3.4.7",
"prisma": "6.15.0",
"proxy": "^2.1.1",
diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts
new file mode 100644
index 000000000000..da278ce61688
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/server.ts
@@ -0,0 +1,37 @@
+import * as Sentry from '@sentry/node';
+import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
+
+export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ release: '1.0',
+ environment: 'prod',
+ // disable requests to /express
+ tracePropagationTargets: [/^(?!.*express).*$/],
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+});
+
+import cors from 'cors';
+import express from 'express';
+import http from 'http';
+
+const app = express();
+
+app.use(cors());
+
+app.get('/test/express-property-values', (req, res) => {
+ const incomingBaggage = req.headers.baggage;
+
+ // Forward the incoming baggage (which contains property values) to the outgoing request
+ // This tests that property values with = signs are preserved during parsing and re-serialization
+ const headers = http.get({ hostname: 'somewhere.not.sentry', headers: { baggage: incomingBaggage } }).getHeaders();
+
+ // Responding with the headers outgoing request headers back to the assertions.
+ res.send({ test_data: headers });
+});
+
+Sentry.setupExpressErrorHandler(app);
+
+startExpressServerAndSendPortToRunner(app);
diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts
new file mode 100644
index 000000000000..23848d36a3df
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-property-values/test.ts
@@ -0,0 +1,28 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+import type { TestAPIResponse } from './server';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('should preserve baggage property values with equal signs (W3C spec compliance)', async () => {
+ const runner = createRunner(__dirname, 'server.ts').start();
+
+ // W3C spec example: https://www.w3.org/TR/baggage/#example
+ const response = await runner.makeRequest('get', '/test/express-property-values', {
+ headers: {
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
+ baggage: 'key1=value1;property1;property2,key2=value2,key3=value3; propertyKey=propertyValue',
+ },
+ });
+
+ expect(response).toBeDefined();
+
+ // The baggage should be parsed and re-serialized, preserving property values with = signs
+ const baggageItems = response?.test_data.baggage?.split(',').map(item => decodeURIComponent(item.trim()));
+
+ expect(baggageItems).toContain('key1=value1;property1;property2');
+ expect(baggageItems).toContain('key2=value2');
+ expect(baggageItems).toContain('key3=value3; propertyKey=propertyValue');
+});
diff --git a/dev-packages/node-integration-tests/suites/pino/instrument.mjs b/dev-packages/node-integration-tests/suites/pino/instrument.mjs
new file mode 100644
index 000000000000..2c09097de1f4
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/pino/instrument.mjs
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/node';
+
+Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: '1.0',
+ enableLogs: true,
+ integrations: [Sentry.pinoIntegration({ error: { levels: ['error', 'fatal'] } })],
+});
diff --git a/dev-packages/node-integration-tests/suites/pino/scenario-next.mjs b/dev-packages/node-integration-tests/suites/pino/scenario-next.mjs
new file mode 100644
index 000000000000..11fc038fea3a
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/pino/scenario-next.mjs
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node';
+import pino from 'pino-next';
+
+const logger = pino({});
+
+Sentry.withIsolationScope(() => {
+ Sentry.startSpan({ name: 'startup' }, () => {
+ logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world');
+ });
+});
+
+setTimeout(() => {
+ Sentry.withIsolationScope(() => {
+ Sentry.startSpan({ name: 'later' }, () => {
+ logger.error(new Error('oh no'));
+ });
+ });
+}, 1000);
diff --git a/dev-packages/node-integration-tests/suites/pino/scenario.mjs b/dev-packages/node-integration-tests/suites/pino/scenario.mjs
new file mode 100644
index 000000000000..3ff6c0b5e08d
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/pino/scenario.mjs
@@ -0,0 +1,18 @@
+import * as Sentry from '@sentry/node';
+import pino from 'pino';
+
+const logger = pino({});
+
+Sentry.withIsolationScope(() => {
+ Sentry.startSpan({ name: 'startup' }, () => {
+ logger.info({ user: 'user-id', something: { more: 3, complex: 'nope' } }, 'hello world');
+ });
+});
+
+setTimeout(() => {
+ Sentry.withIsolationScope(() => {
+ Sentry.startSpan({ name: 'later' }, () => {
+ logger.error(new Error('oh no'));
+ });
+ });
+}, 1000);
diff --git a/dev-packages/node-integration-tests/suites/pino/test.ts b/dev-packages/node-integration-tests/suites/pino/test.ts
new file mode 100644
index 000000000000..15a9397ebb27
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/pino/test.ts
@@ -0,0 +1,172 @@
+import { join } from 'path';
+import { expect, test } from 'vitest';
+import { conditionalTest } from '../../utils';
+import { createRunner } from '../../utils/runner';
+
+conditionalTest({ min: 20 })('Pino integration', () => {
+ test('has different trace ids for logs from different spans', async () => {
+ const instrumentPath = join(__dirname, 'instrument.mjs');
+
+ await createRunner(__dirname, 'scenario.mjs')
+ .withMockSentryServer()
+ .withInstrument(instrumentPath)
+ .ignore('event')
+ .expect({
+ log: log => {
+ const traceId1 = log.items?.[0]?.trace_id;
+ const traceId2 = log.items?.[1]?.trace_id;
+ expect(traceId1).not.toBe(traceId2);
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('captures event and logs', async () => {
+ const instrumentPath = join(__dirname, 'instrument.mjs');
+
+ await createRunner(__dirname, 'scenario.mjs')
+ .withMockSentryServer()
+ .withInstrument(instrumentPath)
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'oh no',
+ mechanism: {
+ type: 'pino',
+ handled: true,
+ },
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ function: '?',
+ in_app: true,
+ module: 'scenario',
+ context_line: " logger.error(new Error('oh no'));",
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .expect({
+ log: {
+ items: [
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'hello world',
+ trace_id: expect.any(String),
+ severity_number: 9,
+ attributes: expect.objectContaining({
+ 'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
+ 'sentry.pino.level': { value: 30, type: 'integer' },
+ user: { value: 'user-id', type: 'string' },
+ something: {
+ type: 'string',
+ value: '{"more":3,"complex":"nope"}',
+ },
+ 'sentry.release': { value: '1.0', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ }),
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'error',
+ body: 'oh no',
+ trace_id: expect.any(String),
+ severity_number: 17,
+ attributes: expect.objectContaining({
+ 'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
+ 'sentry.pino.level': { value: 50, type: 'integer' },
+ err: { value: '{}', type: 'string' },
+ 'sentry.release': { value: '1.0', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ }),
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+ });
+
+ test('captures with Pino integrated channel', async () => {
+ const instrumentPath = join(__dirname, 'instrument.mjs');
+
+ await createRunner(__dirname, 'scenario-next.mjs')
+ .withMockSentryServer()
+ .withInstrument(instrumentPath)
+ .expect({
+ event: {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'oh no',
+ mechanism: {
+ type: 'pino',
+ handled: true,
+ },
+ stacktrace: {
+ frames: expect.arrayContaining([
+ expect.objectContaining({
+ function: '?',
+ in_app: true,
+ module: 'scenario-next',
+ context_line: " logger.error(new Error('oh no'));",
+ }),
+ ]),
+ },
+ },
+ ],
+ },
+ },
+ })
+ .expect({
+ log: {
+ items: [
+ {
+ timestamp: expect.any(Number),
+ level: 'info',
+ body: 'hello world',
+ trace_id: expect.any(String),
+ severity_number: 9,
+ attributes: expect.objectContaining({
+ 'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
+ 'sentry.pino.level': { value: 30, type: 'integer' },
+ user: { value: 'user-id', type: 'string' },
+ something: {
+ type: 'string',
+ value: '{"more":3,"complex":"nope"}',
+ },
+ 'sentry.release': { value: '1.0', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ }),
+ },
+ {
+ timestamp: expect.any(Number),
+ level: 'error',
+ body: 'oh no',
+ trace_id: expect.any(String),
+ severity_number: 17,
+ attributes: expect.objectContaining({
+ 'sentry.origin': { value: 'auto.logging.pino', type: 'string' },
+ 'sentry.pino.level': { value: 50, type: 'integer' },
+ err: { value: '{}', type: 'string' },
+ 'sentry.release': { value: '1.0', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
+ }),
+ },
+ ],
+ },
+ })
+ .start()
+ .completed();
+ });
+});
diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js
new file mode 100644
index 000000000000..7ff548624e5f
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-custom-name.js
@@ -0,0 +1,27 @@
+const Sentry = require('@sentry/node');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.onUnhandledRejectionIntegration({
+ // Use default mode: 'warn' - integration is active but should ignore CustomIgnoredError
+ ignore: [{ name: 'CustomIgnoredError' }],
+ }),
+ ],
+});
+
+// Create a custom error that should be ignored
+class CustomIgnoredError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = 'CustomIgnoredError';
+ }
+}
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+// This should be ignored by the custom ignore matcher and not produce a warning
+Promise.reject(new CustomIgnoredError('This error should be ignored'));
diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js
new file mode 100644
index 000000000000..623aa8eaa8f7
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/ignore-default.js
@@ -0,0 +1,22 @@
+const Sentry = require('@sentry/node');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ // Use default mode: 'warn' - integration is active but should ignore AI_NoOutputGeneratedError
+});
+
+// Create an error with the name that should be ignored by default
+class AI_NoOutputGeneratedError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = 'AI_NoOutputGeneratedError';
+ }
+}
+
+setTimeout(() => {
+ process.stdout.write("I'm alive!");
+ process.exit(0);
+}, 500);
+
+// This should be ignored by default and not produce a warning
+Promise.reject(new AI_NoOutputGeneratedError('Stream aborted'));
diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts
index d3c8b4d599ff..cd0627664ea3 100644
--- a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts
+++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts
@@ -178,4 +178,32 @@ test rejection`);
expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id);
expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id);
});
+
+ test('should not warn when AI_NoOutputGeneratedError is rejected (default ignore)', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'ignore-default.js');
+
+ childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ expect(stderr).toBe(''); // No warning should be shown
+ done();
+ });
+ }));
+
+ test('should not warn when custom ignored error by name is rejected', () =>
+ new Promise(done => {
+ expect.assertions(3);
+
+ const testScriptPath = path.resolve(__dirname, 'ignore-custom-name.js');
+
+ childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
+ expect(err).toBeNull();
+ expect(stdout).toBe("I'm alive!");
+ expect(stderr).toBe(''); // No warning should be shown
+ done();
+ });
+ }));
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-streaming.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-streaming.mjs
new file mode 100644
index 000000000000..be5c75638694
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-streaming.mjs
@@ -0,0 +1,237 @@
+import { GoogleGenAI } from '@google/genai';
+import * as Sentry from '@sentry/node';
+import express from 'express';
+
+function startMockGoogleGenAIServer() {
+ const app = express();
+ app.use(express.json());
+
+ // Streaming endpoint for models.generateContentStream and chat.sendMessageStream
+ app.post('/v1beta/models/:model\\:streamGenerateContent', (req, res) => {
+ const model = req.params.model;
+
+ if (model === 'error-model') {
+ res.status(404).set('x-request-id', 'mock-request-123').end('Model not found');
+ return;
+ }
+
+ // Set headers for streaming response
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Transfer-Encoding', 'chunked');
+
+ // Create a mock stream
+ const mockStream = createMockStream(model);
+
+ // Send chunks
+ const sendChunk = async () => {
+ const { value, done } = await mockStream.next();
+ if (done) {
+ res.end();
+ return;
+ }
+
+ res.write(`data: ${JSON.stringify(value)}\n\n`);
+ setTimeout(sendChunk, 10); // Small delay between chunks
+ };
+
+ sendChunk();
+ });
+
+ return new Promise(resolve => {
+ const server = app.listen(0, () => {
+ resolve(server);
+ });
+ });
+}
+
+// Helper function to create mock stream
+async function* createMockStream(model) {
+ if (model === 'blocked-model') {
+ // First chunk: Contains promptFeedback with blockReason
+ yield {
+ promptFeedback: {
+ blockReason: 'SAFETY',
+ blockReasonMessage: 'The prompt was blocked due to safety concerns',
+ },
+ responseId: 'mock-blocked-response-streaming-id',
+ modelVersion: 'gemini-1.5-pro',
+ };
+
+ // Note: In a real blocked scenario, there would typically be no more chunks
+ // But we'll add one more to test that processing stops after the error
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'This should not be processed' }],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ };
+ return;
+ }
+
+ // First chunk: Start of response with initial text
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'Hello! ' }],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ responseId: 'mock-response-streaming-id',
+ modelVersion: 'gemini-1.5-pro',
+ };
+
+ // Second chunk: More text content
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'This is a streaming ' }],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ };
+
+ // Third chunk: Final text content
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'response from Google GenAI!' }],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ };
+
+ // Final chunk: End with finish reason and usage metadata
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: '' }], // Empty text in final chunk
+ role: 'model',
+ },
+ finishReason: 'STOP',
+ index: 0,
+ },
+ ],
+ usageMetadata: {
+ promptTokenCount: 10,
+ candidatesTokenCount: 12,
+ totalTokenCount: 22,
+ },
+ };
+}
+
+async function run() {
+ const server = await startMockGoogleGenAIServer();
+
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
+ const client = new GoogleGenAI({
+ apiKey: 'mock-api-key',
+ httpOptions: { baseUrl: `http://localhost:${server.address().port}` },
+ });
+
+ // Test 1: models.generateContentStream (streaming)
+ const streamResponse = await client.models.generateContentStream({
+ model: 'gemini-1.5-flash',
+ config: {
+ temperature: 0.7,
+ topP: 0.9,
+ maxOutputTokens: 100,
+ },
+ contents: [
+ {
+ role: 'user',
+ parts: [{ text: 'Tell me about streaming' }],
+ },
+ ],
+ });
+
+ // Consume the stream
+ for await (const _ of streamResponse) {
+ void _;
+ }
+
+ // Test 2: chat.sendMessageStream (streaming)
+ const streamingChat = client.chats.create({
+ model: 'gemini-1.5-pro',
+ config: {
+ temperature: 0.8,
+ topP: 0.9,
+ maxOutputTokens: 150,
+ },
+ });
+
+ const chatStreamResponse = await streamingChat.sendMessageStream({
+ message: 'Tell me a streaming joke',
+ });
+
+ // Consume the chat stream
+ for await (const _ of chatStreamResponse) {
+ void _;
+ }
+
+ // Test 3: Blocked content streaming (should trigger error handling)
+ try {
+ const blockedStreamResponse = await client.models.generateContentStream({
+ model: 'blocked-model',
+ config: {
+ temperature: 0.7,
+ },
+ contents: [
+ {
+ role: 'user',
+ parts: [{ text: 'This should be blocked' }],
+ },
+ ],
+ });
+
+ // Consume the blocked stream
+ for await (const _ of blockedStreamResponse) {
+ void _;
+ }
+ } catch {
+ // Expected: The stream should be processed, but the span should be marked with error status
+ // The error handling happens in the streaming instrumentation, not as a thrown error
+ }
+
+ // Test 4: Error handling for streaming
+ try {
+ const errorStreamResponse = await client.models.generateContentStream({
+ model: 'error-model',
+ config: {
+ temperature: 0.7,
+ },
+ contents: [
+ {
+ role: 'user',
+ parts: [{ text: 'This will fail' }],
+ },
+ ],
+ });
+
+ // Consume the error stream
+ for await (const _ of errorStreamResponse) {
+ void _;
+ }
+ } catch {
+ // Expected error
+ }
+ });
+
+ server.close();
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-tools.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-tools.mjs
new file mode 100644
index 000000000000..97984f2eb1ed
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-tools.mjs
@@ -0,0 +1,307 @@
+import { GoogleGenAI } from '@google/genai';
+import * as Sentry from '@sentry/node';
+import express from 'express';
+
+function startMockGoogleGenAIServer() {
+ const app = express();
+ app.use(express.json());
+
+ // Non-streaming endpoint for models.generateContent
+ app.post('/v1beta/models/:model\\:generateContent', (req, res) => {
+ const { tools } = req.body;
+
+ // Check if tools are provided to return function call response
+ if (tools && tools.length > 0) {
+ const response = {
+ candidates: [
+ {
+ content: {
+ parts: [
+ {
+ text: 'I need to check the light status first.',
+ },
+ {
+ functionCall: {
+ id: 'call_light_control_1',
+ name: 'controlLight',
+ args: {
+ brightness: 0.3,
+ colorTemperature: 'warm',
+ },
+ },
+ },
+ ],
+ role: 'model',
+ },
+ finishReason: 'stop',
+ index: 0,
+ },
+ ],
+ usageMetadata: {
+ promptTokenCount: 15,
+ candidatesTokenCount: 8,
+ totalTokenCount: 23,
+ },
+ };
+
+ // Add functionCalls getter, this should exist in the response object
+ Object.defineProperty(response, 'functionCalls', {
+ get: function () {
+ return [
+ {
+ id: 'call_light_control_1',
+ name: 'controlLight',
+ args: {
+ brightness: 0.3,
+ colorTemperature: 'warm',
+ },
+ },
+ ];
+ },
+ });
+
+ res.send(response);
+ return;
+ }
+
+ // Regular response without tools
+ res.send({
+ candidates: [
+ {
+ content: {
+ parts: [
+ {
+ text: 'Mock response from Google GenAI without tools!',
+ },
+ ],
+ role: 'model',
+ },
+ finishReason: 'stop',
+ index: 0,
+ },
+ ],
+ usageMetadata: {
+ promptTokenCount: 8,
+ candidatesTokenCount: 12,
+ totalTokenCount: 20,
+ },
+ });
+ });
+
+ // Streaming endpoint for models.generateContentStream
+ // And chat.sendMessageStream
+ app.post('/v1beta/models/:model\\:streamGenerateContent', (req, res) => {
+ const { tools } = req.body;
+
+ // Set headers for streaming response
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Transfer-Encoding', 'chunked');
+
+ // Create a mock stream
+ const mockStream = createMockToolsStream({ tools });
+
+ // Send chunks
+ const sendChunk = async () => {
+ // Testing .next() works as expected
+ const { value, done } = await mockStream.next();
+ if (done) {
+ res.end();
+ return;
+ }
+
+ res.write(`data: ${JSON.stringify(value)}\n\n`);
+ setTimeout(sendChunk, 10); // Small delay between chunks
+ };
+
+ sendChunk();
+ });
+
+ return new Promise(resolve => {
+ const server = app.listen(0, () => {
+ resolve(server);
+ });
+ });
+}
+
+// Helper function to create mock stream
+async function* createMockToolsStream({ tools }) {
+ // Check if tools are provided to return function call response
+ if (tools && tools.length > 0) {
+ // First chunk: Text response
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'Let me control the lights for you.' }],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ responseId: 'mock-response-tools-id',
+ modelVersion: 'gemini-2.0-flash-001',
+ };
+
+ // Second chunk: Function call
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [
+ {
+ functionCall: {
+ id: 'call_light_stream_1',
+ name: 'controlLight',
+ args: {
+ brightness: 0.5,
+ colorTemperature: 'cool',
+ },
+ },
+ },
+ ],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ };
+
+ // Final chunk: End with finish reason and usage metadata
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: ' Done!' }], // Additional text in final chunk
+ role: 'model',
+ },
+ finishReason: 'STOP',
+ index: 0,
+ },
+ ],
+ usageMetadata: {
+ promptTokenCount: 12,
+ candidatesTokenCount: 10,
+ totalTokenCount: 22,
+ },
+ };
+ return;
+ }
+
+ // Regular stream without tools
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: 'Mock streaming response' }],
+ role: 'model',
+ },
+ index: 0,
+ },
+ ],
+ responseId: 'mock-response-tools-id',
+ modelVersion: 'gemini-2.0-flash-001',
+ };
+
+ // Final chunk
+ yield {
+ candidates: [
+ {
+ content: {
+ parts: [{ text: ' from Google GenAI!' }],
+ role: 'model',
+ },
+ finishReason: 'STOP',
+ index: 0,
+ },
+ ],
+ usageMetadata: {
+ promptTokenCount: 10,
+ candidatesTokenCount: 12,
+ totalTokenCount: 22,
+ },
+ };
+}
+
+async function run() {
+ const server = await startMockGoogleGenAIServer();
+
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
+ const client = new GoogleGenAI({
+ apiKey: 'mock-api-key',
+ httpOptions: { baseUrl: `http://localhost:${server.address().port}` },
+ });
+
+ // Test 1: Non-streaming with tools
+ await client.models.generateContent({
+ model: 'gemini-2.0-flash-001',
+ contents: 'Dim the lights so the room feels cozy and warm.',
+ config: {
+ tools: [
+ {
+ functionDeclarations: [
+ {
+ name: 'controlLight',
+ parametersJsonSchema: {
+ type: 'object',
+ properties: {
+ brightness: {
+ type: 'number',
+ },
+ colorTemperature: {
+ type: 'string',
+ },
+ },
+ required: ['brightness', 'colorTemperature'],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ // Test 2: Streaming with tools
+ const stream = await client.models.generateContentStream({
+ model: 'gemini-2.0-flash-001',
+ contents: 'Turn on the lights with medium brightness.',
+ config: {
+ tools: [
+ {
+ functionDeclarations: [
+ {
+ name: 'controlLight',
+ parametersJsonSchema: {
+ type: 'object',
+ properties: {
+ brightness: {
+ type: 'number',
+ },
+ colorTemperature: {
+ type: 'string',
+ },
+ },
+ required: ['brightness', 'colorTemperature'],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ // Consume the stream to trigger instrumentation
+ for await (const _ of stream) {
+ void _;
+ }
+
+ // Test 3: Without tools for comparison
+ await client.models.generateContent({
+ model: 'gemini-2.0-flash-001',
+ contents: 'Tell me about the weather.',
+ });
+ });
+
+ server.close();
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs
index ddb9e16b8254..91c75886e410 100644
--- a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario.mjs
@@ -54,6 +54,7 @@ async function run() {
});
// Test 1: chats.create and sendMessage flow
+ // This should generate two spans: one for chats.create and one for sendMessage
const chat = client.chats.create({
model: 'gemini-1.5-pro',
config: {
diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts
index 9aa5523c61d7..92d669c7e10f 100644
--- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts
@@ -169,6 +169,7 @@ describe('Google GenAI integration', () => {
'gen_ai.request.messages': expect.any(String), // Should include messages when recordInputs: true
'gen_ai.response.text': expect.any(String), // Should include response text when recordOutputs: true
}),
+ description: expect.not.stringContaining('stream-response'), // Non-streaming span
}),
]),
};
@@ -202,4 +203,287 @@ describe('Google GenAI integration', () => {
.completed();
});
});
+
+ const EXPECTED_TRANSACTION_TOOLS = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // Non-streaming with tools
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-2.0-flash-001',
+ 'gen_ai.request.available_tools': expect.any(String), // Should include tools
+ 'gen_ai.request.messages': expect.any(String), // Should include contents
+ 'gen_ai.response.text': expect.any(String), // Should include response text
+ 'gen_ai.response.tool_calls': expect.any(String), // Should include tool calls
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 8,
+ 'gen_ai.usage.total_tokens': 23,
+ }),
+ description: 'models gemini-2.0-flash-001',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Streaming with tools
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-2.0-flash-001',
+ 'gen_ai.request.available_tools': expect.any(String), // Should include tools
+ 'gen_ai.request.messages': expect.any(String), // Should include contents
+ 'gen_ai.response.streaming': true,
+ 'gen_ai.response.text': expect.any(String), // Should include response text
+ 'gen_ai.response.tool_calls': expect.any(String), // Should include tool calls
+ 'gen_ai.response.id': 'mock-response-tools-id',
+ 'gen_ai.response.model': 'gemini-2.0-flash-001',
+ 'gen_ai.usage.input_tokens': 12,
+ 'gen_ai.usage.output_tokens': 10,
+ 'gen_ai.usage.total_tokens': 22,
+ }),
+ description: 'models gemini-2.0-flash-001 stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Without tools for comparison
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-2.0-flash-001',
+ 'gen_ai.request.messages': expect.any(String), // Should include contents
+ 'gen_ai.response.text': expect.any(String), // Should include response text
+ 'gen_ai.usage.input_tokens': 8,
+ 'gen_ai.usage.output_tokens': 12,
+ 'gen_ai.usage.total_tokens': 20,
+ }),
+ description: 'models gemini-2.0-flash-001',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ ]),
+ };
+
+ createEsmAndCjsTests(__dirname, 'scenario-tools.mjs', 'instrument-with-options.mjs', (createRunner, test) => {
+ test('creates google genai related spans with tool calls', async () => {
+ await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_TOOLS }).start().completed();
+ });
+ });
+
+ const EXPECTED_TRANSACTION_STREAMING = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // First span - models.generateContentStream (streaming)
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-1.5-flash',
+ 'gen_ai.request.temperature': 0.7,
+ 'gen_ai.request.top_p': 0.9,
+ 'gen_ai.request.max_tokens': 100,
+ 'gen_ai.response.streaming': true,
+ 'gen_ai.response.id': 'mock-response-streaming-id',
+ 'gen_ai.response.model': 'gemini-1.5-pro',
+ 'gen_ai.response.finish_reasons': '["STOP"]',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 12,
+ 'gen_ai.usage.total_tokens': 22,
+ }),
+ description: 'models gemini-1.5-flash stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Second span - chat.create
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'chat',
+ 'sentry.op': 'gen_ai.chat',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-1.5-pro',
+ 'gen_ai.request.temperature': 0.8,
+ 'gen_ai.request.top_p': 0.9,
+ 'gen_ai.request.max_tokens': 150,
+ }),
+ description: 'chat gemini-1.5-pro create',
+ op: 'gen_ai.chat',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Third span - chat.sendMessageStream (streaming)
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'chat',
+ 'sentry.op': 'gen_ai.chat',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-1.5-pro',
+ 'gen_ai.response.streaming': true,
+ 'gen_ai.response.id': 'mock-response-streaming-id',
+ 'gen_ai.response.model': 'gemini-1.5-pro',
+ }),
+ description: 'chat gemini-1.5-pro stream-response',
+ op: 'gen_ai.chat',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Fourth span - blocked content streaming
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ }),
+ description: 'models blocked-model stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'unknown_error',
+ }),
+ // Fifth span - error handling for streaming
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ }),
+ description: 'models error-model stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'internal_error',
+ }),
+ ]),
+ };
+
+ const EXPECTED_TRANSACTION_STREAMING_PII_TRUE = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // First span - models.generateContentStream (streaming) with PII
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-1.5-flash',
+ 'gen_ai.request.temperature': 0.7,
+ 'gen_ai.request.top_p': 0.9,
+ 'gen_ai.request.max_tokens': 100,
+ 'gen_ai.request.messages': expect.any(String), // Should include contents when recordInputs: true
+ 'gen_ai.response.streaming': true,
+ 'gen_ai.response.id': 'mock-response-streaming-id',
+ 'gen_ai.response.model': 'gemini-1.5-pro',
+ 'gen_ai.response.finish_reasons': '["STOP"]',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 12,
+ 'gen_ai.usage.total_tokens': 22,
+ }),
+ description: 'models gemini-1.5-flash stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Second span - chat.create
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'chat',
+ 'sentry.op': 'gen_ai.chat',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-1.5-pro',
+ 'gen_ai.request.temperature': 0.8,
+ 'gen_ai.request.top_p': 0.9,
+ 'gen_ai.request.max_tokens': 150,
+ }),
+ description: 'chat gemini-1.5-pro create',
+ op: 'gen_ai.chat',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Third span - chat.sendMessageStream (streaming) with PII
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'chat',
+ 'sentry.op': 'gen_ai.chat',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'gemini-1.5-pro',
+ 'gen_ai.request.messages': expect.any(String), // Should include message when recordInputs: true
+ 'gen_ai.response.streaming': true,
+ 'gen_ai.response.id': 'mock-response-streaming-id',
+ 'gen_ai.response.model': 'gemini-1.5-pro',
+ 'gen_ai.response.finish_reasons': '["STOP"]',
+ 'gen_ai.usage.input_tokens': 10,
+ 'gen_ai.usage.output_tokens': 12,
+ 'gen_ai.usage.total_tokens': 22,
+ }),
+ description: 'chat gemini-1.5-pro stream-response',
+ op: 'gen_ai.chat',
+ origin: 'auto.ai.google_genai',
+ status: 'ok',
+ }),
+ // Fourth span - blocked content stream with PII
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'blocked-model',
+ 'gen_ai.request.temperature': 0.7,
+ 'gen_ai.request.messages': expect.any(String), // Should include contents when recordInputs: true
+ 'gen_ai.response.streaming': true,
+ }),
+ description: 'models blocked-model stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'unknown_error',
+ }),
+ // Fifth span - error handling for streaming with PII
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'gen_ai.operation.name': 'models',
+ 'sentry.op': 'gen_ai.models',
+ 'sentry.origin': 'auto.ai.google_genai',
+ 'gen_ai.system': 'google_genai',
+ 'gen_ai.request.model': 'error-model',
+ 'gen_ai.request.temperature': 0.7,
+ 'gen_ai.request.messages': expect.any(String), // Should include contents when recordInputs: true
+ }),
+ description: 'models error-model stream-response',
+ op: 'gen_ai.models',
+ origin: 'auto.ai.google_genai',
+ status: 'internal_error',
+ }),
+ ]),
+ };
+
+ createEsmAndCjsTests(__dirname, 'scenario-streaming.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('creates google genai streaming spans with sendDefaultPii: false', async () => {
+ await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_STREAMING }).start().completed();
+ });
+ });
+
+ createEsmAndCjsTests(__dirname, 'scenario-streaming.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
+ test('creates google genai streaming spans with sendDefaultPii: true', async () => {
+ await createRunner()
+ .ignore('event')
+ .expect({ transaction: EXPECTED_TRANSACTION_STREAMING_PII_TRUE })
+ .start()
+ .completed();
+ });
+ });
});
diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-generate-object.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-generate-object.mjs
new file mode 100644
index 000000000000..64d0d3ba0ec7
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-generate-object.mjs
@@ -0,0 +1,30 @@
+import * as Sentry from '@sentry/node';
+import { generateObject } from 'ai';
+import { MockLanguageModelV1 } from 'ai/test';
+import { z } from 'zod';
+
+async function run() {
+ await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
+ // Test generateObject with schema
+ await generateObject({
+ model: new MockLanguageModelV1({
+ defaultObjectGenerationMode: 'json',
+ doGenerate: async () => ({
+ rawCall: { rawPrompt: null, rawSettings: {} },
+ finishReason: 'stop',
+ usage: { promptTokens: 15, completionTokens: 25 },
+ text: '{ "name": "John Doe", "age": 30 }',
+ }),
+ }),
+ schema: z.object({
+ name: z.string().describe('The name of the person'),
+ age: z.number().describe('The age of the person'),
+ }),
+ schemaName: 'Person',
+ schemaDescription: 'A person with name and age',
+ prompt: 'Generate a person object',
+ });
+ });
+}
+
+run();
diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts
new file mode 100644
index 000000000000..1e87b63535ac
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test-generate-object.ts
@@ -0,0 +1,67 @@
+import { afterAll, describe, expect } from 'vitest';
+import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
+
+describe('Vercel AI integration - generateObject', () => {
+ afterAll(() => {
+ cleanupChildProcesses();
+ });
+
+ const EXPECTED_TRANSACTION = {
+ transaction: 'main',
+ spans: expect.arrayContaining([
+ // generateObject span
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.operationId': 'ai.generateObject',
+ 'vercel.ai.pipeline.name': 'generateObject',
+ 'vercel.ai.streaming': false,
+ 'vercel.ai.settings.mode': 'json',
+ 'vercel.ai.settings.output': 'object',
+ 'gen_ai.request.schema': expect.any(String),
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ 'operation.name': 'ai.generateObject',
+ 'sentry.op': 'gen_ai.invoke_agent',
+ 'sentry.origin': 'auto.vercelai.otel',
+ }),
+ description: 'generateObject',
+ op: 'gen_ai.invoke_agent',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ // generateObject.doGenerate span
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.vercelai.otel',
+ 'sentry.op': 'gen_ai.generate_object',
+ 'operation.name': 'ai.generateObject.doGenerate',
+ 'vercel.ai.operationId': 'ai.generateObject.doGenerate',
+ 'vercel.ai.model.provider': 'mock-provider',
+ 'vercel.ai.model.id': 'mock-model-id',
+ 'vercel.ai.pipeline.name': 'generateObject.doGenerate',
+ 'vercel.ai.streaming': false,
+ 'gen_ai.system': 'mock-provider',
+ 'gen_ai.request.model': 'mock-model-id',
+ 'gen_ai.response.model': 'mock-model-id',
+ 'gen_ai.usage.input_tokens': 15,
+ 'gen_ai.usage.output_tokens': 25,
+ 'gen_ai.usage.total_tokens': 40,
+ }),
+ description: 'generate_object mock-model-id',
+ op: 'gen_ai.generate_object',
+ origin: 'auto.vercelai.otel',
+ status: 'ok',
+ }),
+ ]),
+ };
+
+ createEsmAndCjsTests(__dirname, 'scenario-generate-object.mjs', 'instrument.mjs', (createRunner, test) => {
+ test('captures generateObject spans with schema attributes', async () => {
+ await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
+ });
+ });
+});
diff --git a/package.json b/package.json
index e74379564683..edbd645b3c97 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,6 @@
"packages/node-native",
"packages/nuxt",
"packages/opentelemetry",
- "packages/pino-transport",
"packages/profiling-node",
"packages/react",
"packages/react-router",
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index 790810e93797..f70d6e0a3573 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -94,6 +94,7 @@ export {
onUnhandledRejectionIntegration,
openAIIntegration,
parameterize,
+ pinoIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index f7e72ec908ae..5a608a925edb 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -106,6 +106,7 @@ export {
mysql2Integration,
redisIntegration,
tediousIntegration,
+ pinoIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
diff --git a/packages/core/src/utils/baggage.ts b/packages/core/src/utils/baggage.ts
index b483207ba8f2..e94bb3d896e6 100644
--- a/packages/core/src/utils/baggage.ts
+++ b/packages/core/src/utils/baggage.ts
@@ -113,8 +113,15 @@ export function parseBaggageHeader(
function baggageHeaderToObject(baggageHeader: string): Record {
return baggageHeader
.split(',')
- .map(baggageEntry =>
- baggageEntry.split('=').map(keyOrValue => {
+ .map(baggageEntry => {
+ const eqIdx = baggageEntry.indexOf('=');
+ if (eqIdx === -1) {
+ // Likely an invalid entry
+ return [];
+ }
+ const key = baggageEntry.slice(0, eqIdx);
+ const value = baggageEntry.slice(eqIdx + 1);
+ return [key, value].map(keyOrValue => {
try {
return decodeURIComponent(keyOrValue.trim());
} catch {
@@ -122,8 +129,8 @@ function baggageHeaderToObject(baggageHeader: string): Record {
// This will then be skipped in the next step
return;
}
- }),
- )
+ });
+ })
.reduce>((acc, [key, value]) => {
if (key && value) {
acc[key] = value;
diff --git a/packages/core/src/utils/google-genai/constants.ts b/packages/core/src/utils/google-genai/constants.ts
index 8617460482c6..b06e46e18755 100644
--- a/packages/core/src/utils/google-genai/constants.ts
+++ b/packages/core/src/utils/google-genai/constants.ts
@@ -2,7 +2,15 @@ export const GOOGLE_GENAI_INTEGRATION_NAME = 'Google_GenAI';
// https://ai.google.dev/api/rest/v1/models/generateContent
// https://ai.google.dev/api/rest/v1/chats/sendMessage
-export const GOOGLE_GENAI_INSTRUMENTED_METHODS = ['models.generateContent', 'chats.create', 'sendMessage'] as const;
+// https://googleapis.github.io/js-genai/release_docs/classes/models.Models.html#generatecontentstream
+// https://googleapis.github.io/js-genai/release_docs/classes/chats.Chat.html#sendmessagestream
+export const GOOGLE_GENAI_INSTRUMENTED_METHODS = [
+ 'models.generateContent',
+ 'models.generateContentStream',
+ 'chats.create',
+ 'sendMessage',
+ 'sendMessageStream',
+] as const;
// Constants for internal use
export const GOOGLE_GENAI_SYSTEM_NAME = 'google_genai';
diff --git a/packages/core/src/utils/google-genai/index.ts b/packages/core/src/utils/google-genai/index.ts
index 58d7e2e6b5e6..20e6e2a53606 100644
--- a/packages/core/src/utils/google-genai/index.ts
+++ b/packages/core/src/utils/google-genai/index.ts
@@ -1,10 +1,12 @@
import { getClient } from '../../currentScopes';
import { captureException } from '../../exports';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
-import { startSpan } from '../../tracing/trace';
+import { SPAN_STATUS_ERROR } from '../../tracing';
+import { startSpan, startSpanManual } from '../../tracing/trace';
import type { Span, SpanAttributeValue } from '../../types-hoist/span';
import {
GEN_AI_OPERATION_NAME_ATTRIBUTE,
+ GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE,
GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE,
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
GEN_AI_REQUEST_MESSAGES_ATTRIBUTE,
@@ -14,6 +16,7 @@ import {
GEN_AI_REQUEST_TOP_K_ATTRIBUTE,
GEN_AI_REQUEST_TOP_P_ATTRIBUTE,
GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
+ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
GEN_AI_SYSTEM_ATTRIBUTE,
GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
@@ -22,6 +25,7 @@ import {
import { buildMethodPath, getFinalOperationName, getSpanOperation } from '../ai/utils';
import { handleCallbackErrors } from '../handleCallbackErrors';
import { CHAT_PATH, CHATS_CREATE_METHOD, GOOGLE_GENAI_SYSTEM_NAME } from './constants';
+import { instrumentStream } from './streaming';
import type {
Candidate,
ContentPart,
@@ -29,7 +33,7 @@ import type {
GoogleGenAIOptions,
GoogleGenAIResponse,
} from './types';
-import { shouldInstrument } from './utils';
+import { isStreamingMethod, shouldInstrument } from './utils';
/**
* Extract model from parameters or chat context object
@@ -91,8 +95,8 @@ function extractConfigAttributes(config: Record): Record,
context?: unknown,
): Record {
const attributes: Record = {
@@ -101,14 +105,21 @@ function extractRequestAttributes(
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
};
- if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
- const params = args[0] as Record;
-
+ if (params) {
attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel(params, context);
// Extract generation config parameters
if ('config' in params && typeof params.config === 'object' && params.config) {
- Object.assign(attributes, extractConfigAttributes(params.config as Record));
+ const config = params.config as Record;
+ Object.assign(attributes, extractConfigAttributes(config));
+
+ // Extract available tools from config
+ if ('tools' in config && Array.isArray(config.tools)) {
+ const functionDeclarations = config.tools.map(
+ (tool: { functionDeclarations: unknown[] }) => tool.functionDeclarations,
+ );
+ attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = JSON.stringify(functionDeclarations);
+ }
}
} else {
attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel({}, context);
@@ -186,6 +197,16 @@ function addResponseAttributes(span: Span, response: GoogleGenAIResponse, record
});
}
}
+
+ // Add tool calls if recordOutputs is enabled
+ if (recordOutputs && response.functionCalls) {
+ const functionCalls = response.functionCalls;
+ if (Array.isArray(functionCalls) && functionCalls.length > 0) {
+ span.setAttributes({
+ [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: JSON.stringify(functionCalls),
+ });
+ }
+ }
}
/**
@@ -201,43 +222,75 @@ function instrumentMethod(
): (...args: T) => R | Promise {
const isSyncCreate = methodPath === CHATS_CREATE_METHOD;
- const run = (...args: T): R | Promise => {
- const requestAttributes = extractRequestAttributes(args, methodPath, context);
- const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
- const operationName = getFinalOperationName(methodPath);
-
- // Single span for both sync and async operations
- return startSpan(
- {
- name: isSyncCreate ? `${operationName} ${model} create` : `${operationName} ${model}`,
- op: getSpanOperation(methodPath),
- attributes: requestAttributes,
- },
- (span: Span) => {
- if (options.recordInputs && args[0] && typeof args[0] === 'object') {
- addPrivateRequestAttributes(span, args[0] as Record);
- }
-
- return handleCallbackErrors(
- () => originalMethod.apply(context, args),
- error => {
- captureException(error, {
- mechanism: { handled: false, type: 'auto.ai.google_genai', data: { function: methodPath } },
- });
+ return new Proxy(originalMethod, {
+ apply(target, _, args: T): R | Promise {
+ const params = args[0] as Record | undefined;
+ const requestAttributes = extractRequestAttributes(methodPath, params, context);
+ const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
+ const operationName = getFinalOperationName(methodPath);
+
+ // Check if this is a streaming method
+ if (isStreamingMethod(methodPath)) {
+ // Use startSpanManual for streaming methods to control span lifecycle
+ return startSpanManual(
+ {
+ name: `${operationName} ${model} stream-response`,
+ op: getSpanOperation(methodPath),
+ attributes: requestAttributes,
},
- () => {},
- result => {
- // Only add response attributes for content-producing methods, not for chats.create
- if (!isSyncCreate) {
- addResponseAttributes(span, result, options.recordOutputs);
+ async (span: Span) => {
+ try {
+ if (options.recordInputs && params) {
+ addPrivateRequestAttributes(span, params);
+ }
+ const stream = await target.apply(context, args);
+ return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R;
+ } catch (error) {
+ span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ type: 'auto.ai.google_genai',
+ data: { function: methodPath },
+ },
+ });
+ span.end();
+ throw error;
}
},
);
- },
- );
- };
-
- return run;
+ }
+ // Single span for both sync and async operations
+ return startSpan(
+ {
+ name: isSyncCreate ? `${operationName} ${model} create` : `${operationName} ${model}`,
+ op: getSpanOperation(methodPath),
+ attributes: requestAttributes,
+ },
+ (span: Span) => {
+ if (options.recordInputs && params) {
+ addPrivateRequestAttributes(span, params);
+ }
+
+ return handleCallbackErrors(
+ () => target.apply(context, args),
+ error => {
+ captureException(error, {
+ mechanism: { handled: false, type: 'auto.ai.google_genai', data: { function: methodPath } },
+ });
+ },
+ () => {},
+ result => {
+ // Only add response attributes for content-producing methods, not for chats.create
+ if (!isSyncCreate) {
+ addResponseAttributes(span, result, options.recordOutputs);
+ }
+ },
+ );
+ },
+ );
+ },
+ }) as (...args: T) => R | Promise;
}
/**
diff --git a/packages/core/src/utils/google-genai/streaming.ts b/packages/core/src/utils/google-genai/streaming.ts
new file mode 100644
index 000000000000..b9462e8c90dd
--- /dev/null
+++ b/packages/core/src/utils/google-genai/streaming.ts
@@ -0,0 +1,163 @@
+import { captureException } from '../../exports';
+import { SPAN_STATUS_ERROR } from '../../tracing';
+import type { Span, SpanAttributeValue } from '../../types-hoist/span';
+import {
+ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE,
+ GEN_AI_RESPONSE_ID_ATTRIBUTE,
+ GEN_AI_RESPONSE_MODEL_ATTRIBUTE,
+ GEN_AI_RESPONSE_STREAMING_ATTRIBUTE,
+ GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
+ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
+ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
+ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
+} from '../ai/gen-ai-attributes';
+import type { GoogleGenAIResponse } from './types';
+
+/**
+ * State object used to accumulate information from a stream of Google GenAI events.
+ */
+interface StreamingState {
+ /** Collected response text fragments (for output recording). */
+ responseTexts: string[];
+ /** Reasons for finishing the response, as reported by the API. */
+ finishReasons: string[];
+ /** The response ID. */
+ responseId?: string;
+ /** The model name. */
+ responseModel?: string;
+ /** Number of prompt/input tokens used. */
+ promptTokens?: number;
+ /** Number of completion/output tokens used. */
+ completionTokens?: number;
+ /** Number of total tokens used. */
+ totalTokens?: number;
+ /** Accumulated tool calls (finalized) */
+ toolCalls: Array>;
+}
+
+/**
+ * Checks if a response chunk contains an error
+ * @param chunk - The response chunk to check
+ * @param span - The span to update if error is found
+ * @returns Whether an error occurred
+ */
+function isErrorChunk(chunk: GoogleGenAIResponse, span: Span): boolean {
+ const feedback = chunk?.promptFeedback;
+ if (feedback?.blockReason) {
+ const message = feedback.blockReasonMessage ?? feedback.blockReason;
+ span.setStatus({ code: SPAN_STATUS_ERROR, message: `Content blocked: ${message}` });
+ captureException(`Content blocked: ${message}`, {
+ mechanism: { handled: false, type: 'auto.ai.google_genai' },
+ });
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Processes response metadata from a chunk
+ * @param chunk - The response chunk to process
+ * @param state - The state of the streaming process
+ */
+function handleResponseMetadata(chunk: GoogleGenAIResponse, state: StreamingState): void {
+ if (typeof chunk.responseId === 'string') state.responseId = chunk.responseId;
+ if (typeof chunk.modelVersion === 'string') state.responseModel = chunk.modelVersion;
+
+ const usage = chunk.usageMetadata;
+ if (usage) {
+ if (typeof usage.promptTokenCount === 'number') state.promptTokens = usage.promptTokenCount;
+ if (typeof usage.candidatesTokenCount === 'number') state.completionTokens = usage.candidatesTokenCount;
+ if (typeof usage.totalTokenCount === 'number') state.totalTokens = usage.totalTokenCount;
+ }
+}
+
+/**
+ * Processes candidate content from a response chunk
+ * @param chunk - The response chunk to process
+ * @param state - The state of the streaming process
+ * @param recordOutputs - Whether to record outputs
+ */
+function handleCandidateContent(chunk: GoogleGenAIResponse, state: StreamingState, recordOutputs: boolean): void {
+ if (Array.isArray(chunk.functionCalls)) {
+ state.toolCalls.push(...chunk.functionCalls);
+ }
+
+ for (const candidate of chunk.candidates ?? []) {
+ if (candidate?.finishReason && !state.finishReasons.includes(candidate.finishReason)) {
+ state.finishReasons.push(candidate.finishReason);
+ }
+
+ for (const part of candidate?.content?.parts ?? []) {
+ if (recordOutputs && part.text) state.responseTexts.push(part.text);
+ if (part.functionCall) {
+ state.toolCalls.push({
+ type: 'function',
+ id: part.functionCall.id,
+ name: part.functionCall.name,
+ arguments: part.functionCall.args,
+ });
+ }
+ }
+ }
+}
+
+/**
+ * Processes a single chunk from the Google GenAI stream
+ * @param chunk - The chunk to process
+ * @param state - The state of the streaming process
+ * @param recordOutputs - Whether to record outputs
+ * @param span - The span to update
+ */
+function processChunk(chunk: GoogleGenAIResponse, state: StreamingState, recordOutputs: boolean, span: Span): void {
+ if (!chunk || isErrorChunk(chunk, span)) return;
+ handleResponseMetadata(chunk, state);
+ handleCandidateContent(chunk, state, recordOutputs);
+}
+
+/**
+ * Instruments an async iterable stream of Google GenAI response chunks, updates the span with
+ * streaming attributes and (optionally) the aggregated output text, and yields
+ * each chunk from the input stream unchanged.
+ */
+export async function* instrumentStream(
+ stream: AsyncIterable,
+ span: Span,
+ recordOutputs: boolean,
+): AsyncGenerator {
+ const state: StreamingState = {
+ responseTexts: [],
+ finishReasons: [],
+ toolCalls: [],
+ };
+
+ try {
+ for await (const chunk of stream) {
+ processChunk(chunk, state, recordOutputs, span);
+ yield chunk;
+ }
+ } finally {
+ const attrs: Record = {
+ [GEN_AI_RESPONSE_STREAMING_ATTRIBUTE]: true,
+ };
+
+ if (state.responseId) attrs[GEN_AI_RESPONSE_ID_ATTRIBUTE] = state.responseId;
+ if (state.responseModel) attrs[GEN_AI_RESPONSE_MODEL_ATTRIBUTE] = state.responseModel;
+ if (state.promptTokens !== undefined) attrs[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = state.promptTokens;
+ if (state.completionTokens !== undefined) attrs[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = state.completionTokens;
+ if (state.totalTokens !== undefined) attrs[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE] = state.totalTokens;
+
+ if (state.finishReasons.length) {
+ attrs[GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE] = JSON.stringify(state.finishReasons);
+ }
+ if (recordOutputs && state.responseTexts.length) {
+ attrs[GEN_AI_RESPONSE_TEXT_ATTRIBUTE] = state.responseTexts.join('');
+ }
+ if (recordOutputs && state.toolCalls.length) {
+ attrs[GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE] = JSON.stringify(state.toolCalls);
+ }
+
+ span.setAttributes(attrs);
+ span.end();
+ }
+}
diff --git a/packages/core/src/utils/google-genai/utils.ts b/packages/core/src/utils/google-genai/utils.ts
index c7a18477c7dd..a394ed64a1bb 100644
--- a/packages/core/src/utils/google-genai/utils.ts
+++ b/packages/core/src/utils/google-genai/utils.ts
@@ -14,3 +14,14 @@ export function shouldInstrument(methodPath: string): methodPath is GoogleGenAII
const methodName = methodPath.split('.').pop();
return GOOGLE_GENAI_INSTRUMENTED_METHODS.includes(methodName as GoogleGenAIIstrumentedMethod);
}
+
+/**
+ * Check if a method is a streaming method
+ */
+export function isStreamingMethod(methodPath: string): boolean {
+ return (
+ methodPath.includes('Stream') ||
+ methodPath.endsWith('generateContentStream') ||
+ methodPath.endsWith('sendMessageStream')
+ );
+}
diff --git a/packages/core/src/utils/vercel-ai/index.ts b/packages/core/src/utils/vercel-ai/index.ts
index 912dcaee3bc4..9b1cc2bc8aae 100644
--- a/packages/core/src/utils/vercel-ai/index.ts
+++ b/packages/core/src/utils/vercel-ai/index.ts
@@ -17,6 +17,7 @@ import {
AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE,
AI_RESPONSE_TEXT_ATTRIBUTE,
AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
+ AI_SCHEMA_ATTRIBUTE,
AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
AI_TOOL_CALL_ARGS_ATTRIBUTE,
AI_TOOL_CALL_ID_ATTRIBUTE,
@@ -125,6 +126,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void {
renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input');
renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output');
+ renameAttributeKey(attributes, AI_SCHEMA_ATTRIBUTE, 'gen_ai.request.schema');
+
addProviderMetadataToAttributes(attributes);
// Change attributes namespaced with `ai.X` to `vercel.ai.X`
diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts
index e2f1ad5fc2b2..2eb7f39f3a24 100644
--- a/packages/core/src/utils/worldwide.ts
+++ b/packages/core/src/utils/worldwide.ts
@@ -48,6 +48,8 @@ export type InternalGlobal = {
*/
_sentryModuleMetadata?: Record;
_sentryEsmLoaderHookRegistered?: boolean;
+ _sentryInjectLoaderHookRegister?: () => void;
+ _sentryInjectLoaderHookRegistered?: boolean;
} & Carrier;
/** Get's the global object for the current JavaScript runtime */
diff --git a/packages/core/test/lib/utils/baggage.test.ts b/packages/core/test/lib/utils/baggage.test.ts
index 4816a3fbf079..f3717a524bf8 100644
--- a/packages/core/test/lib/utils/baggage.test.ts
+++ b/packages/core/test/lib/utils/baggage.test.ts
@@ -71,4 +71,16 @@ describe('parseBaggageHeader', () => {
const actual = parseBaggageHeader(input);
expect(actual).toStrictEqual(expectedOutput);
});
+
+ test('should preserve property values with equal signs', () => {
+ // see https://www.w3.org/TR/baggage/#example
+ const baggageHeader = 'key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue';
+ const result = parseBaggageHeader(baggageHeader);
+
+ expect(result).toStrictEqual({
+ key1: 'value1;property1;property2',
+ key2: 'value2',
+ key3: 'value3; propertyKey=propertyValue',
+ });
+ });
});
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index bab9dc3a1cbb..8f1d236f7877 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -106,6 +106,7 @@ export {
mysql2Integration,
redisIntegration,
tediousIntegration,
+ pinoIntegration,
postgresIntegration,
postgresJsIntegration,
prismaIntegration,
diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts
index f8076087fd5d..92c90c3719de 100644
--- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts
+++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts
@@ -74,10 +74,18 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
return decoratorResult(target, propertyKey, descriptor);
}
+ function eventNameFromEvent(event: unknown): string {
+ if (typeof event === 'string') {
+ return event;
+ } else if (Array.isArray(event)) {
+ return event.map(eventNameFromEvent).join(',');
+ } else return String(event);
+ }
+
const originalHandler = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const handlerName = originalHandler.name || propertyKey;
- let eventName = typeof event === 'string' ? event : String(event);
+ let eventName = eventNameFromEvent(event);
// Instrument the actual handler
descriptor.value = async function (...args: unknown[]) {
@@ -93,7 +101,7 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
eventName = eventData
.map((data: unknown) => {
if (data && typeof data === 'object' && 'event' in data && data.event) {
- return data.event;
+ return eventNameFromEvent(data.event);
}
return '';
})
diff --git a/packages/nestjs/test/integrations/nest.test.ts b/packages/nestjs/test/integrations/nest.test.ts
index 69fb022441dd..2d1d73b4657a 100644
--- a/packages/nestjs/test/integrations/nest.test.ts
+++ b/packages/nestjs/test/integrations/nest.test.ts
@@ -75,17 +75,72 @@ describe('Nest', () => {
await descriptor.value();
- expect(core.startSpan).toHaveBeenCalled();
+ expect(core.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'event test.event',
+ }),
+ expect.any(Function),
+ );
expect(originalHandler).toHaveBeenCalled();
});
- it('should wrap array event handlers', async () => {
+ it('should wrap symbol event handlers', async () => {
+ const decorated = wrappedOnEvent(Symbol('test.event'));
+ decorated(mockTarget, 'testMethod', descriptor);
+
+ await descriptor.value();
+
+ expect(core.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'event Symbol(test.event)',
+ }),
+ expect.any(Function),
+ );
+ expect(originalHandler).toHaveBeenCalled();
+ });
+
+ it('should wrap string array event handlers', async () => {
const decorated = wrappedOnEvent(['test.event1', 'test.event2']);
decorated(mockTarget, 'testMethod', descriptor);
await descriptor.value();
- expect(core.startSpan).toHaveBeenCalled();
+ expect(core.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'event test.event1,test.event2',
+ }),
+ expect.any(Function),
+ );
+ expect(originalHandler).toHaveBeenCalled();
+ });
+
+ it('should wrap symbol array event handlers', async () => {
+ const decorated = wrappedOnEvent([Symbol('test.event1'), Symbol('test.event2')]);
+ decorated(mockTarget, 'testMethod', descriptor);
+
+ await descriptor.value();
+
+ expect(core.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'event Symbol(test.event1),Symbol(test.event2)',
+ }),
+ expect.any(Function),
+ );
+ expect(originalHandler).toHaveBeenCalled();
+ });
+
+ it('should wrap mixed type array event handlers', async () => {
+ const decorated = wrappedOnEvent([Symbol('test.event1'), 'test.event2', Symbol('test.event3')]);
+ decorated(mockTarget, 'testMethod', descriptor);
+
+ await descriptor.value();
+
+ expect(core.startSpan).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'event Symbol(test.event1),test.event2,Symbol(test.event3)',
+ }),
+ expect.any(Function),
+ );
expect(originalHandler).toHaveBeenCalled();
});
diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
index 4cf8fde751fb..8f02df798f84 100644
--- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
@@ -15,7 +15,6 @@ import {
} from '@sentry/core';
import type { NextApiRequest } from 'next';
import type { AugmentedNextApiResponse, NextApiHandler } from '../types';
-import { addHeadersAsAttributes } from '../utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../utils/responseEnd';
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';
@@ -88,7 +87,6 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
- ...addHeadersAsAttributes(normalizedRequest.headers || {}),
},
},
async span => {
diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
index 323d4d1f2e3b..c22910df43bf 100644
--- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
+++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
@@ -22,7 +22,6 @@ import {
import type { GenerationFunctionContext } from '../common/types';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
-import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { getSanitizedRequestUrl } from './utils/urls';
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
@@ -64,11 +63,6 @@ export function wrapGenerationFunctionWithSentry a
const headersDict = headers ? winterCGHeadersToDict(headers) : undefined;
- if (activeSpan) {
- const rootSpan = getRootSpan(activeSpan);
- addHeadersAsAttributes(headers, rootSpan);
- }
-
let data: Record | undefined = undefined;
if (getClient()?.getOptions().sendDefaultPii) {
const props: unknown = args[0];
diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
index ab05fbd5e944..07694d659e57 100644
--- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
+++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
@@ -13,7 +13,6 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
-import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import type { EdgeRouteHandler } from '../edge/types';
@@ -60,7 +59,6 @@ export function wrapMiddlewareWithSentry(
let spanName: string;
let spanSource: TransactionSource;
- let headerAttributes: Record = {};
if (req instanceof Request) {
isolationScope.setSDKProcessingMetadata({
@@ -68,8 +66,6 @@ export function wrapMiddlewareWithSentry(
});
spanName = `middleware ${req.method} ${new URL(req.url).pathname}`;
spanSource = 'url';
-
- headerAttributes = addHeadersAsAttributes(req.headers);
} else {
spanName = 'middleware';
spanSource = 'component';
@@ -88,7 +84,6 @@ export function wrapMiddlewareWithSentry(
const rootSpan = getRootSpan(activeSpan);
if (rootSpan) {
setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope);
- rootSpan.setAttributes(headerAttributes);
}
}
@@ -99,7 +94,6 @@ export function wrapMiddlewareWithSentry(
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrap_middleware',
- ...headerAttributes,
},
},
() => {
diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
index 54858a9bdae2..068ab7960ae4 100644
--- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
@@ -19,7 +19,6 @@ import {
} from '@sentry/core';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import type { RouteHandlerContext } from './types';
-import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from './utils/responseEnd';
import { commonObjectToIsolationScope } from './utils/tracingUtils';
@@ -40,10 +39,6 @@ export function wrapRouteHandlerWithSentry any>(
const activeSpan = getActiveSpan();
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
- if (rootSpan && process.env.NEXT_RUNTIME !== 'edge') {
- addHeadersAsAttributes(headers, rootSpan);
- }
-
let edgeRuntimeIsolationScopeOverride: Scope | undefined;
if (rootSpan && process.env.NEXT_RUNTIME === 'edge') {
const isolationScope = commonObjectToIsolationScope(headers);
@@ -55,7 +50,6 @@ export function wrapRouteHandlerWithSentry any>(
rootSpan.updateName(`${method} ${parameterizedRoute}`);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
- addHeadersAsAttributes(headers, rootSpan);
}
return withIsolationScope(
diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
index d25225a149f9..1f522dbf212f 100644
--- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
@@ -24,7 +24,6 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/
import type { ServerComponentContext } from '../common/types';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
-import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { getSanitizedRequestUrl } from './utils/urls';
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
@@ -62,11 +61,6 @@ export function wrapServerComponentWithSentry any>
const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;
- if (activeSpan) {
- const rootSpan = getRootSpan(activeSpan);
- addHeadersAsAttributes(context.headers, rootSpan);
- }
-
let params: Record | undefined = undefined;
if (getClient()?.getOptions().sendDefaultPii) {
diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts
index 7982667f0c3f..6469e3c6a2c8 100644
--- a/packages/nextjs/src/edge/index.ts
+++ b/packages/nextjs/src/edge/index.ts
@@ -1,6 +1,7 @@
import {
applySdkMetadata,
getGlobalScope,
+ getIsolationScope,
getRootSpan,
GLOBAL_OBJ,
registerSpanErrorInstrumentation,
@@ -13,6 +14,7 @@ import {
} from '@sentry/core';
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
+import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { isBuild } from '../common/utils/isBuild';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
@@ -59,6 +61,8 @@ export function init(options: VercelEdgeOptions = {}): void {
client?.on('spanStart', span => {
const spanAttributes = spanToJSON(span).data;
+ const rootSpan = getRootSpan(span);
+ const isRootSpan = span === rootSpan;
// Mark all spans generated by Next.js as 'auto'
if (spanAttributes?.['next.span_type'] !== undefined) {
@@ -70,6 +74,12 @@ export function init(options: VercelEdgeOptions = {}): void {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
}
+
+ if (isRootSpan) {
+ // todo: check if we can set request headers for edge on sdkProcessingMetadata
+ const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers;
+ addHeadersAsAttributes(headers, rootSpan);
+ }
});
// Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most
diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
index 735caf8d7788..9d3d4e4427fa 100644
--- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
@@ -39,7 +39,6 @@ export function wrapApiHandlerWithSentry(
normalizedRequest: winterCGRequestToRequestData(req),
});
currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`);
-
headerAttributes = addHeadersAsAttributes(req.headers);
} else {
currentScope.setTransactionName(`handler (${parameterizedRoute})`);
diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts
index 5866f014ec69..5ce23e6a9460 100644
--- a/packages/nextjs/src/server/index.ts
+++ b/packages/nextjs/src/server/index.ts
@@ -36,6 +36,7 @@ import {
TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL,
TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION,
} from '../common/span-attributes-with-logic-attached';
+import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { isBuild } from '../common/utils/isBuild';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
@@ -163,13 +164,13 @@ export function init(options: NodeOptions): NodeClient | undefined {
client?.on('spanStart', span => {
const spanAttributes = spanToJSON(span).data;
+ const rootSpan = getRootSpan(span);
+ const isRootSpan = span === rootSpan;
// What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted
// by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute.
if (typeof spanAttributes?.['next.route'] === 'string') {
- const rootSpan = getRootSpan(span);
const rootSpanAttributes = spanToJSON(rootSpan).data;
-
// Only hoist the http.route attribute if the transaction doesn't already have it
if (
// eslint-disable-next-line deprecation/deprecation
@@ -190,8 +191,13 @@ export function init(options: NodeOptions): NodeClient | undefined {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
}
+ if (isRootSpan) {
+ const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers;
+ addHeadersAsAttributes(headers, rootSpan);
+ }
+
// We want to fork the isolation scope for incoming requests
- if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && span === getRootSpan(span)) {
+ if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && isRootSpan) {
const scopes = getCapturedScopesOnSpan(span);
const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
diff --git a/packages/node-core/package.json b/packages/node-core/package.json
index 4e83faeb767d..ed90625bac44 100644
--- a/packages/node-core/package.json
+++ b/packages/node-core/package.json
@@ -68,9 +68,11 @@
"dependencies": {
"@sentry/core": "10.17.0",
"@sentry/opentelemetry": "10.17.0",
+ "@apm-js-collab/tracing-hooks": "^0.3.1",
"import-in-the-middle": "^1.14.2"
},
"devDependencies": {
+ "@apm-js-collab/code-transformer": "^0.8.2",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.1.0",
"@opentelemetry/core": "^2.1.0",
diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts
index e6cf209d23f6..0f976bd23436 100644
--- a/packages/node-core/src/index.ts
+++ b/packages/node-core/src/index.ts
@@ -27,6 +27,7 @@ export { spotlightIntegration } from './integrations/spotlight';
export { systemErrorIntegration } from './integrations/systemError';
export { childProcessIntegration } from './integrations/childProcess';
export { createSentryWinstonTransport } from './integrations/winston';
+export { pinoIntegration } from './integrations/pino';
export { SentryContextManager } from './otel/contextManager';
export { setupOpenTelemetryLogger } from './otel/logger';
diff --git a/packages/node-core/src/integrations/onunhandledrejection.ts b/packages/node-core/src/integrations/onunhandledrejection.ts
index dbddb2a4c396..42a17e2e6c7e 100644
--- a/packages/node-core/src/integrations/onunhandledrejection.ts
+++ b/packages/node-core/src/integrations/onunhandledrejection.ts
@@ -1,24 +1,41 @@
import type { Client, IntegrationFn, SeverityLevel, Span } from '@sentry/core';
-import { captureException, consoleSandbox, defineIntegration, getClient, withActiveSpan } from '@sentry/core';
+import {
+ captureException,
+ consoleSandbox,
+ defineIntegration,
+ getClient,
+ isMatchingPattern,
+ withActiveSpan,
+} from '@sentry/core';
import { logAndExitProcess } from '../utils/errorhandling';
type UnhandledRejectionMode = 'none' | 'warn' | 'strict';
+type IgnoreMatcher = { name?: string | RegExp; message?: string | RegExp };
+
interface OnUnhandledRejectionOptions {
/**
* Option deciding what to do after capturing unhandledRejection,
* that mimicks behavior of node's --unhandled-rejection flag.
*/
mode: UnhandledRejectionMode;
+ /** Rejection Errors to ignore (don't capture or warn). */
+ ignore?: IgnoreMatcher[];
}
const INTEGRATION_NAME = 'OnUnhandledRejection';
+const DEFAULT_IGNORES: IgnoreMatcher[] = [
+ {
+ name: 'AI_NoOutputGeneratedError', // When stream aborts in Vercel AI SDK, Vercel flush() fails with an error
+ },
+];
+
const _onUnhandledRejectionIntegration = ((options: Partial = {}) => {
- const opts = {
- mode: 'warn',
- ...options,
- } satisfies OnUnhandledRejectionOptions;
+ const opts: OnUnhandledRejectionOptions = {
+ mode: options.mode ?? 'warn',
+ ignore: [...DEFAULT_IGNORES, ...(options.ignore ?? [])],
+ };
return {
name: INTEGRATION_NAME,
@@ -28,27 +45,54 @@ const _onUnhandledRejectionIntegration = ((options: Partial;
+ const name = typeof errorLike.name === 'string' ? errorLike.name : '';
+ const message = typeof errorLike.message === 'string' ? errorLike.message : String(reason);
+
+ return { name, message };
+}
+
+/** Check if a matcher matches the reason */
+function isMatchingReason(matcher: IgnoreMatcher, errorInfo: ReturnType): boolean {
+ // name/message matcher
+ const nameMatches = matcher.name === undefined || isMatchingPattern(errorInfo.name, matcher.name, true);
+
+ const messageMatches = matcher.message === undefined || isMatchingPattern(errorInfo.message, matcher.message);
+
+ return nameMatches && messageMatches;
+}
+
+/** Match helper */
+function matchesIgnore(list: IgnoreMatcher[], reason: unknown): boolean {
+ const errorInfo = extractErrorInfo(reason);
+ return list.some(matcher => isMatchingReason(matcher, errorInfo));
+}
+
+/** Core handler */
export function makeUnhandledPromiseHandler(
client: Client,
options: OnUnhandledRejectionOptions,
): (reason: unknown, promise: unknown) => void {
return function sendUnhandledPromise(reason: unknown, promise: unknown): void {
+ // Only handle for the active client
if (getClient() !== client) {
return;
}
+ // Skip if configured to ignore
+ if (matchesIgnore(options.ignore ?? [], reason)) {
+ return;
+ }
+
const level: SeverityLevel = options.mode === 'strict' ? 'fatal' : 'error';
// this can be set in places where we cannot reliably get access to the active span/error
diff --git a/packages/node-core/src/integrations/pino.ts b/packages/node-core/src/integrations/pino.ts
new file mode 100644
index 000000000000..af3f41735c4a
--- /dev/null
+++ b/packages/node-core/src/integrations/pino.ts
@@ -0,0 +1,149 @@
+import { tracingChannel } from 'node:diagnostics_channel';
+import type { IntegrationFn, LogSeverityLevel } from '@sentry/core';
+import {
+ _INTERNAL_captureLog,
+ addExceptionMechanism,
+ captureException,
+ captureMessage,
+ defineIntegration,
+ severityLevelFromString,
+ withScope,
+} from '@sentry/core';
+import { addInstrumentationConfig } from '../sdk/injectLoader';
+
+type LevelMapping = {
+ // Fortunately pino uses the same levels as Sentry
+ labels: { [level: number]: LogSeverityLevel };
+};
+
+type Pino = {
+ levels: LevelMapping;
+};
+
+type MergeObject = {
+ [key: string]: unknown;
+ err?: Error;
+};
+
+type PinoHookArgs = [MergeObject, string, number];
+
+type PinoOptions = {
+ error: {
+ /**
+ * Levels that trigger capturing of events.
+ *
+ * @default []
+ */
+ levels: LogSeverityLevel[];
+ /**
+ * By default, Sentry will mark captured errors as handled.
+ * Set this to `false` if you want to mark them as unhandled instead.
+ *
+ * @default true
+ */
+ handled: boolean;
+ };
+ log: {
+ /**
+ * Levels that trigger capturing of logs. Logs are only captured if
+ * `enableLogs` is enabled.
+ *
+ * @default ["trace", "debug", "info", "warn", "error", "fatal"]
+ */
+ levels: LogSeverityLevel[];
+ };
+};
+
+const DEFAULT_OPTIONS: PinoOptions = {
+ error: { levels: [], handled: true },
+ log: { levels: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] },
+};
+
+type DeepPartial = {
+ [P in keyof T]?: T[P] extends object ? Partial : T[P];
+};
+
+/**
+ * Integration for Pino logging library.
+ * Captures Pino logs as Sentry logs and optionally captures some log levels as events.
+ *
+ * Requires Pino >=v8.0.0 and Node >=20.6.0 or >=18.19.0
+ */
+export const pinoIntegration = defineIntegration((userOptions: DeepPartial = {}) => {
+ const options: PinoOptions = {
+ error: { ...DEFAULT_OPTIONS.error, ...userOptions.error },
+ log: { ...DEFAULT_OPTIONS.log, ...userOptions.log },
+ };
+
+ return {
+ name: 'Pino',
+ setup: client => {
+ const enableLogs = !!client.getOptions().enableLogs;
+
+ addInstrumentationConfig({
+ channelName: 'pino-log',
+ // From Pino v9.10.0 a tracing channel is available directly from Pino:
+ // https://github.com/pinojs/pino/pull/2281
+ module: { name: 'pino', versionRange: '>=8.0.0 < 9.10.0', filePath: 'lib/tools.js' },
+ functionQuery: {
+ functionName: 'asJson',
+ kind: 'Sync',
+ },
+ });
+
+ const injectedChannel = tracingChannel('orchestrion:pino:pino-log');
+ const integratedChannel = tracingChannel('pino_asJson');
+
+ function onPinoStart(self: Pino, args: PinoHookArgs): void {
+ const [obj, message, levelNumber] = args;
+ const level = self?.levels?.labels?.[levelNumber] || 'info';
+
+ const attributes = {
+ ...obj,
+ 'sentry.origin': 'auto.logging.pino',
+ 'sentry.pino.level': levelNumber,
+ };
+
+ if (enableLogs && options.log.levels.includes(level)) {
+ _INTERNAL_captureLog({ level, message, attributes });
+ }
+
+ if (options.error.levels.includes(level)) {
+ const captureContext = {
+ level: severityLevelFromString(level),
+ };
+
+ withScope(scope => {
+ scope.addEventProcessor(event => {
+ event.logger = 'pino';
+
+ addExceptionMechanism(event, {
+ handled: options.error.handled,
+ type: 'pino',
+ });
+
+ return event;
+ });
+
+ if (obj.err) {
+ captureException(obj.err, captureContext);
+ return;
+ }
+
+ captureMessage(message, captureContext);
+ });
+ }
+ }
+
+ injectedChannel.start.subscribe(data => {
+ const { self, arguments: args } = data as { self: Pino; arguments: PinoHookArgs };
+ onPinoStart(self, args);
+ });
+
+ integratedChannel.start.subscribe(data => {
+ const { instance, arguments: args } = data as { instance: Pino; arguments: PinoHookArgs };
+ onPinoStart(instance, args);
+ });
+ },
+ };
+}) satisfies IntegrationFn;
diff --git a/packages/node-core/src/sdk/apm-js-collab-tracing-hooks.d.ts b/packages/node-core/src/sdk/apm-js-collab-tracing-hooks.d.ts
new file mode 100644
index 000000000000..c4ae4897678d
--- /dev/null
+++ b/packages/node-core/src/sdk/apm-js-collab-tracing-hooks.d.ts
@@ -0,0 +1,11 @@
+declare module '@apm-js-collab/tracing-hooks' {
+ import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';
+
+ type PatchConfig = { instrumentations: InstrumentationConfig[] };
+
+ /** Hooks require */
+ export default class ModulePatch {
+ public constructor(config: PatchConfig): ModulePatch;
+ public patch(): void;
+ }
+}
diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts
index c4a16d76a1d0..d53f5d4faefb 100644
--- a/packages/node-core/src/sdk/index.ts
+++ b/packages/node-core/src/sdk/index.ts
@@ -7,6 +7,7 @@ import {
functionToStringIntegration,
getCurrentScope,
getIntegrationsToSetup,
+ GLOBAL_OBJ,
hasSpansEnabled,
inboundFiltersIntegration,
linkedErrorsIntegration,
@@ -131,6 +132,8 @@ function _init(
client.init();
+ GLOBAL_OBJ._sentryInjectLoaderHookRegister?.();
+
debug.log(`SDK initialized from ${isCjs() ? 'CommonJS' : 'ESM'}`);
client.startClientReportTracking();
diff --git a/packages/node-core/src/sdk/injectLoader.ts b/packages/node-core/src/sdk/injectLoader.ts
new file mode 100644
index 000000000000..667996ebbe53
--- /dev/null
+++ b/packages/node-core/src/sdk/injectLoader.ts
@@ -0,0 +1,46 @@
+import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';
+import ModulePatch from '@apm-js-collab/tracing-hooks';
+import { debug, GLOBAL_OBJ } from '@sentry/core';
+import * as moduleModule from 'module';
+import { supportsEsmLoaderHooks } from '../utils/detection';
+
+let instrumentationConfigs: InstrumentationConfig[] | undefined;
+
+/**
+ * Add an instrumentation config to be used by the injection loader.
+ */
+export function addInstrumentationConfig(config: InstrumentationConfig): void {
+ if (!supportsEsmLoaderHooks()) {
+ return;
+ }
+
+ if (!instrumentationConfigs) {
+ instrumentationConfigs = [];
+ }
+
+ instrumentationConfigs.push(config);
+
+ GLOBAL_OBJ._sentryInjectLoaderHookRegister = () => {
+ if (GLOBAL_OBJ._sentryInjectLoaderHookRegistered) {
+ return;
+ }
+
+ GLOBAL_OBJ._sentryInjectLoaderHookRegistered = true;
+
+ const instrumentations = instrumentationConfigs || [];
+
+ // Patch require to support CJS modules
+ const requirePatch = new ModulePatch({ instrumentations });
+ requirePatch.patch();
+
+ // Add ESM loader to support ESM modules
+ try {
+ // @ts-expect-error register is available in these versions
+ moduleModule.register('@apm-js-collab/tracing-hooks/hook.mjs', import.meta.url, {
+ data: { instrumentations },
+ });
+ } catch (error) {
+ debug.warn("Failed to register '@apm-js-collab/tracing-hooks' hook", error);
+ }
+ };
+}
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 4808f22b472b..db378e55f6ca 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -171,6 +171,7 @@ export {
disableAnrDetectionForCallback,
spotlightIntegration,
childProcessIntegration,
+ pinoIntegration,
createSentryWinstonTransport,
SentryContextManager,
systemErrorIntegration,
diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts
index 7e9445a154a7..1e806e4dc2eb 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -72,6 +72,18 @@ export default defineNuxtModule({
mode: 'client',
order: 1,
});
+
+ // Add the sentry config file to the include array
+ nuxt.hook('prepare:types', options => {
+ if (!options.tsConfig.include) {
+ options.tsConfig.include = [];
+ }
+
+ // Add type references for useRuntimeConfig in root files for nuxt v4
+ // Should be relative to `root/.nuxt`
+ const relativePath = path.relative(nuxt.options.buildDir, clientConfigFile);
+ options.tsConfig.include.push(relativePath);
+ });
}
const serverConfigFile = findDefaultSdkInitFile('server', nuxt);
diff --git a/packages/pino-transport/.eslintrc.js b/packages/pino-transport/.eslintrc.js
deleted file mode 100644
index 01c6be4c7080..000000000000
--- a/packages/pino-transport/.eslintrc.js
+++ /dev/null
@@ -1,12 +0,0 @@
-module.exports = {
- env: {
- node: true,
- },
- extends: ['../../.eslintrc.js'],
- overrides: [
- {
- files: ['src/**/*.ts'],
- rules: {},
- },
- ],
-};
diff --git a/packages/pino-transport/LICENSE b/packages/pino-transport/LICENSE
deleted file mode 100644
index 5251db3eaaca..000000000000
--- a/packages/pino-transport/LICENSE
+++ /dev/null
@@ -1,16 +0,0 @@
-MIT License
-
-Copyright (c) 2025 Functional Software, Inc. dba Sentry
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
-persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
-Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/pino-transport/README.md b/packages/pino-transport/README.md
deleted file mode 100644
index fdc077aa2b01..000000000000
--- a/packages/pino-transport/README.md
+++ /dev/null
@@ -1,266 +0,0 @@
-
-
-
-
-
-
-# Official Sentry Pino Transport
-
-[](https://www.npmjs.com/package/@sentry/solid)
-[](https://www.npmjs.com/package/@sentry/solid)
-[](https://www.npmjs.com/package/@sentry/solid)
-
-**WARNING**: This transport is in a **pre-release alpha**. The API is unstable and may change at any time.
-
-A Pino transport for sending logs to Sentry using the Sentry JavaScript SDK.
-
-This transport forwards Pino logs to Sentry, allowing you to view and analyze your application logs alongside your errors and performance data in Sentry.
-
-## Installation
-
-```bash
-npm install @sentry/pino-transport pino
-# or
-yarn add @sentry/pino-transport pino
-# or
-pnpm add @sentry/pino-transport pino
-```
-
-## Requirements
-
-- Node.js 18+
-- Pino v8 or v9
-- `@sentry/node` SDK with `enableLogs: true`
-
-## Setup
-
-First, make sure Sentry is initialized with logging enabled:
-
-```javascript
-import * as Sentry from '@sentry/node';
-
-Sentry.init({
- dsn: 'YOUR_DSN',
- enableLogs: true,
-});
-```
-
-Then create a Pino logger with the Sentry transport:
-
-```javascript
-import pino from 'pino';
-
-const logger = pino({
- transport: {
- target: '@sentry/pino-transport',
- options: {
- // Optional: filter which log levels to send to Sentry
- levels: ['error', 'fatal'], // defaults to all levels
- },
- },
-});
-
-// Now your logs will be sent to Sentry
-logger.info('This is an info message');
-logger.error('This is an error message');
-```
-
-## Configuration Options
-
-The transport accepts the following options:
-
-### `logLevels`
-
-**Type:** `Array<'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'>`
-
-**Default:** `['trace', 'debug', 'info', 'warn', 'error', 'fatal']` (all log levels)
-
-Use this option to filter which log severity levels should be sent to Sentry.
-
-```javascript
-const transport = pino.transport({
- target: '@sentry/pino-transport',
- options: {
- logLevels: ['warn', 'error', 'fatal'], // Only send warnings and above
- },
-});
-```
-
-## Log Level Mapping
-
-Pino log levels are automatically mapped to Sentry log severity levels:
-
-| Pino Level | Pino Numeric | Sentry Level |
-| ---------- | ------------ | ------------ |
-| trace | 10 | trace |
-| debug | 20 | debug |
-| info | 30 | info |
-| warn | 40 | warn |
-| error | 50 | error |
-| fatal | 60 | fatal |
-
-### Custom Levels Support
-
-Custom numeric levels are mapped to Sentry levels using ranges, so levels like `11`, `23`, or `42` will map correctly:
-
-- `0-19` → `trace`
-- `20-29` → `debug`
-- `30-39` → `info`
-- `40-49` → `warn`
-- `50-59` → `error`
-- `60+` → `fatal`
-
-```javascript
-import pino from 'pino';
-
-const logger = pino({
- customLevels: {
- critical: 55, // Maps to 'fatal' (55+ range)
- notice: 35, // Maps to 'warn' (35-44 range)
- verbose: 11, // Maps to 'trace' (0-14 range)
- },
- transport: {
- target: '@sentry/pino-transport',
- },
-});
-
-logger.critical('Critical issue occurred'); // → Sent as 'fatal' to Sentry
-logger.notice('Important notice'); // → Sent as 'warn' to Sentry
-logger.verbose('Detailed information'); // → Sent as 'trace' to Sentry
-```
-
-#### Custom Level Attributes
-
-When using custom string levels, the original level name is preserved as `sentry.pino.level` attribute for better traceability:
-
-```javascript
-// Log entry in Sentry will include:
-// {
-// level: 'warn', // Mapped Sentry level
-// message: 'Audit event',
-// attributes: {
-// 'sentry.pino.level': 'audit', // Original custom level name
-// 'sentry.origin': 'auto.logging.pino',
-// // ... other log attributes
-// }
-// }
-```
-
-### Custom Message Key
-
-The transport respects Pino's `messageKey` configuration:
-
-```javascript
-const logger = pino({
- messageKey: 'message', // Use 'message' instead of default 'msg'
- transport: {
- target: '@sentry/pino-transport',
- },
-});
-
-logger.info({ message: 'Hello world' }); // Works correctly with custom messageKey
-```
-
-### Nested Key Support
-
-The transport automatically supports Pino's `nestedKey` configuration, which is used to avoid property conflicts by nesting logged objects under a specific key. When `nestedKey` is configured, the transport flattens these nested properties using dot notation for better searchability in Sentry.
-
-```javascript
-const logger = pino({
- nestedKey: 'payload', // Nest logged objects under 'payload' key
- transport: {
- target: '@sentry/pino-transport',
- },
-});
-
-const conflictingObject = {
- level: 'hi', // Conflicts with Pino's level
- time: 'never', // Conflicts with Pino's time
- foo: 'bar',
- userId: 123,
-};
-
-logger.info(conflictingObject);
-
-// Without nestedKey, this would cause property conflicts
-// With nestedKey, Pino creates: { level: 30, time: 1234567890, payload: conflictingObject }
-// The transport flattens it to:
-// {
-// level: 'info',
-// message: undefined,
-// attributes: {
-// 'payload.level': 'hi', // Flattened nested properties
-// 'payload.time': 'never',
-// 'payload.foo': 'bar',
-// 'payload.userId': 123,
-// 'sentry.origin': 'auto.logging.pino',
-// }
-// }
-```
-
-This flattening ensures that no property conflicts occur between logged objects and Pino's internal properties.
-
-## Usage Examples
-
-### Basic Logging
-
-```javascript
-import pino from 'pino';
-
-const logger = pino({
- transport: {
- target: '@sentry/pino-transport',
- },
-});
-
-logger.trace('Starting application');
-logger.debug('Debug information', { userId: 123 });
-logger.info('User logged in', { userId: 123, username: 'john_doe' });
-logger.warn('Deprecated API used', { endpoint: '/old-api' });
-logger.error('Database connection failed', { error: 'Connection timeout' });
-logger.fatal('Application crashed', { reason: 'Out of memory' });
-```
-
-### Multiple Transports
-
-```javascript
-import pino from 'pino';
-
-const logger = pino({
- transport: {
- targets: [
- {
- target: 'pino-pretty',
- options: { colorize: true },
- level: 'debug',
- },
- {
- target: '@sentry/pino-transport',
- options: {
- logLevels: ['warn', 'error', 'fatal'],
- },
- level: 'warn',
- },
- ],
- },
-});
-```
-
-## Troubleshooting
-
-### Logs not appearing in Sentry
-
-1. Ensure `enableLogs: true` is set in your Sentry configuration.
-2. Check that your DSN is correct and the SDK is properly initialized.
-3. Verify the log level is included in the `levels` configuration.
-4. Check your Sentry organization stats page to see if logs are being received by Sentry.
-
-## Related Documentation
-
-- [Sentry Logs Documentation](https://docs.sentry.io/platforms/javascript/guides/node/logs/)
-- [Pino Documentation](https://getpino.io/)
-- [Pino Transports](https://getpino.io/#/docs/transports)
-
-## License
-
-MIT
diff --git a/packages/pino-transport/package.json b/packages/pino-transport/package.json
deleted file mode 100644
index 13c83d19b6ee..000000000000
--- a/packages/pino-transport/package.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "name": "@sentry/pino-transport",
- "version": "10.17.0",
- "description": "Pino transport for Sentry SDK",
- "repository": "git://github.com/getsentry/sentry-javascript.git",
- "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/pino-transport",
- "author": "Sentry",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "files": [
- "/build"
- ],
- "main": "build/cjs/index.js",
- "module": "build/esm/index.js",
- "types": "build/types/index.d.ts",
- "exports": {
- "./package.json": "./package.json",
- ".": {
- "import": {
- "types": "./build/types/index.d.ts",
- "default": "./build/esm/index.js"
- },
- "require": {
- "types": "./build/types/index.d.ts",
- "default": "./build/cjs/index.js"
- }
- }
- },
- "typesVersions": {
- "<5.0": {
- "build/types/index.d.ts": [
- "build/types-ts3.8/index.d.ts"
- ]
- }
- },
- "publishConfig": {
- "access": "public"
- },
- "dependencies": {
- "@sentry/core": "10.17.0",
- "@sentry/node": "10.17.0",
- "pino-abstract-transport": "^2.0.0"
- },
- "peerDependencies": {
- "pino": "^8.0.0 || ^9.0.0"
- },
- "devDependencies": {
- "@types/node": "^18.19.1",
- "pino": "^9.0.0"
- },
- "scripts": {
- "build": "run-p build:transpile build:types",
- "build:dev": "yarn build",
- "build:transpile": "rollup -c rollup.npm.config.mjs",
- "build:types": "run-s build:types:core build:types:downlevel",
- "build:types:core": "tsc -p tsconfig.types.json",
- "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8",
- "build:watch": "run-p build:transpile:watch build:types:watch",
- "build:dev:watch": "yarn build:watch",
- "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
- "build:types:watch": "tsc -p tsconfig.types.json --watch",
- "build:tarball": "npm pack",
- "circularDepCheck": "madge --circular src/index.ts",
- "clean": "rimraf build coverage sentry-pino-transport-*.tgz",
- "fix": "eslint . --format stylish --fix",
- "lint": "eslint . --format stylish",
- "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module",
- "test": "yarn test:unit",
- "test:unit": "vitest run",
- "test:watch": "vitest --watch",
- "yalc:publish": "yalc publish --push --sig"
- },
- "volta": {
- "extends": "../../package.json"
- },
- "sideEffects": false
-}
diff --git a/packages/pino-transport/rollup.npm.config.mjs b/packages/pino-transport/rollup.npm.config.mjs
deleted file mode 100644
index 84a06f2fb64a..000000000000
--- a/packages/pino-transport/rollup.npm.config.mjs
+++ /dev/null
@@ -1,3 +0,0 @@
-import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
-
-export default makeNPMConfigVariants(makeBaseNPMConfig());
diff --git a/packages/pino-transport/src/debug-build.ts b/packages/pino-transport/src/debug-build.ts
deleted file mode 100644
index 60aa50940582..000000000000
--- a/packages/pino-transport/src/debug-build.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-declare const __DEBUG_BUILD__: boolean;
-
-/**
- * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
- *
- * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
- */
-export const DEBUG_BUILD = __DEBUG_BUILD__;
diff --git a/packages/pino-transport/src/index.ts b/packages/pino-transport/src/index.ts
deleted file mode 100644
index 986c7e892fc2..000000000000
--- a/packages/pino-transport/src/index.ts
+++ /dev/null
@@ -1,244 +0,0 @@
-import type { LogSeverityLevel } from '@sentry/core';
-import { _INTERNAL_captureLog, debug, isPrimitive, normalize } from '@sentry/core';
-import type buildType from 'pino-abstract-transport';
-import * as pinoAbstractTransport from 'pino-abstract-transport';
-import { DEBUG_BUILD } from './debug-build';
-
-// Handle both CommonJS and ES module exports
-// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
-const build = (pinoAbstractTransport as any).default || pinoAbstractTransport;
-
-/**
- * The default log levels that will be captured by the Sentry Pino transport.
- */
-const DEFAULT_CAPTURED_LEVELS: Array = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
-
-/**
- * Options for the Sentry Pino transport.
- */
-export interface SentryPinoTransportOptions {
- /**
- * Use this option to filter which levels should be captured as logs.
- * By default, all levels are captured as logs.
- *
- * @example
- * ```ts
- * const logger = pino({
- * transport: {
- * target: '@sentry/pino-transport',
- * options: {
- * logLevels: ['error', 'warn'], // Only capture error and warn logs
- * },
- * },
- * });
- * ```
- */
- logLevels?: Array;
-}
-
-/**
- * Pino source configuration passed to the transport.
- * This interface represents the configuration options that Pino provides to transports.
- */
-interface PinoSourceConfig {
- /**
- * Custom levels configuration from Pino.
- * Contains the mapping of custom level names to numeric values.
- *
- * @default undefined
- * @example { values: { critical: 55, notice: 35 } }
- */
- levels?: unknown;
-
- /**
- * The property name used for the log message.
- * Pino allows customizing which property contains the main log message.
- *
- * @default 'msg'
- * @example 'message' when configured with messageKey: 'message'
- * @see https://getpino.io/#/docs/api?id=messagekey-string
- */
- messageKey?: string;
-
- /**
- * The property name used for error objects.
- * Pino allows customizing which property contains error information.
- *
- * @default 'err'
- * @example 'error' when configured with errorKey: 'error'
- * @see https://getpino.io/#/docs/api?id=errorkey-string
- */
- errorKey?: string;
-
- /**
- * The property name used to nest logged objects to avoid conflicts.
- * When set, Pino nests all logged objects under this key to prevent
- * conflicts with Pino's internal properties (level, time, pid, etc.).
- * The transport flattens these nested properties using dot notation.
- *
- * @default undefined (no nesting)
- * @example 'payload' - objects logged will be nested under { payload: {...} }
- * @see https://getpino.io/#/docs/api?id=nestedkey-string
- */
- nestedKey?: string;
-}
-
-/**
- * Creates a new Sentry Pino transport that forwards logs to Sentry. Requires the `enableLogs` option to be enabled.
- *
- * Supports Pino v8 and v9.
- *
- * @param options - Options for the transport.
- * @returns A Pino transport that forwards logs to Sentry.
- *
- * @experimental This method will experience breaking changes. This is not yet part of
- * the stable Sentry SDK API and can be changed or removed without warning.
- */
-export function createSentryPinoTransport(options?: SentryPinoTransportOptions): ReturnType {
- DEBUG_BUILD && debug.log('Initializing Sentry Pino transport');
- const capturedLogLevels = new Set(options?.logLevels ?? DEFAULT_CAPTURED_LEVELS);
-
- return build(
- async function (source: AsyncIterable & PinoSourceConfig) {
- for await (const log of source) {
- try {
- if (!isObject(log)) {
- continue;
- }
-
- // Use Pino's messageKey if available, fallback to 'msg'
- const messageKey = source.messageKey || 'msg';
- const message = log[messageKey];
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { [messageKey]: _, level, time, ...attributes } = log;
-
- // Handle nestedKey flattening if configured
- if (source.nestedKey && attributes[source.nestedKey] && isObject(attributes[source.nestedKey])) {
- const nestedObject = attributes[source.nestedKey] as Record;
- // Remove the nested object and flatten its properties
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
- delete attributes[source.nestedKey];
-
- // Flatten nested properties with dot notation
- for (const [key, value] of Object.entries(nestedObject)) {
- attributes[`${source.nestedKey}.${key}`] = value;
- }
- }
-
- const logSeverityLevel = mapPinoLevelToSentryLevel(log.level, source.levels);
-
- if (capturedLogLevels.has(logSeverityLevel)) {
- const logAttributes: Record = {
- ...attributes,
- 'sentry.origin': 'auto.logging.pino',
- };
-
- // Attach custom level as an attribute if it's a string (custom level)
- if (typeof log.level === 'string') {
- logAttributes['sentry.pino.level'] = log.level;
- }
-
- _INTERNAL_captureLog({
- level: logSeverityLevel,
- message: formatMessage(message),
- attributes: logAttributes,
- });
- }
- } catch {
- // Silently ignore errors to prevent breaking the logging pipeline
- }
- }
- },
- {
- expectPinoConfig: true,
- },
- );
-}
-
-function formatMessage(message: unknown): string {
- if (message === undefined) {
- return '';
- }
-
- if (isPrimitive(message)) {
- return String(message);
- }
- return JSON.stringify(normalize(message));
-}
-
-/**
- * Maps a Pino log level (numeric or custom string) to a Sentry log severity level.
- *
- * Handles both standard and custom levels, including when `useOnlyCustomLevels` is enabled.
- * Uses range-based mapping for numeric levels to handle custom values (e.g., 11 -> trace).
- */
-function mapPinoLevelToSentryLevel(level: unknown, levelsConfig?: unknown): LogSeverityLevel {
- // Handle numeric levels
- if (typeof level === 'number') {
- return mapNumericLevelToSentryLevel(level);
- }
-
- // Handle custom string levels
- if (
- typeof level === 'string' &&
- isObject(levelsConfig) &&
- 'values' in levelsConfig &&
- isObject(levelsConfig.values)
- ) {
- // Map custom string levels to numeric then to Sentry levels
- const numericLevel = levelsConfig.values[level];
- if (typeof numericLevel === 'number') {
- return mapNumericLevelToSentryLevel(numericLevel);
- }
- }
-
- // Default fallback
- return 'info';
-}
-
-/**
- * Maps a numeric level to the closest Sentry severity level using range-based mapping.
- * Handles both standard Pino levels and custom numeric levels.
- *
- * - `0-19` -> `trace`
- * - `20-29` -> `debug`
- * - `30-39` -> `info`
- * - `40-49` -> `warn`
- * - `50-59` -> `error`
- * - `60+` -> `fatal`
- *
- * @see https://github.com/pinojs/pino/blob/116b1b17935630b97222fbfd1c053d199d18ca4b/lib/constants.js#L6-L13
- */
-function mapNumericLevelToSentryLevel(numericLevel: number): LogSeverityLevel {
- // 0-19 -> trace
- if (numericLevel < 20) {
- return 'trace';
- }
- // 20-29 -> debug
- if (numericLevel < 30) {
- return 'debug';
- }
- // 30-39 -> info
- if (numericLevel < 40) {
- return 'info';
- }
- // 40-49 -> warn
- if (numericLevel < 50) {
- return 'warn';
- }
- // 50-59 -> error
- if (numericLevel < 60) {
- return 'error';
- }
- // 60+ -> fatal
- return 'fatal';
-}
-
-/**
- * Type guard to check if a value is an object.
- */
-function isObject(value: unknown): value is Record {
- return typeof value === 'object' && value != null;
-}
-
-export default createSentryPinoTransport;
diff --git a/packages/pino-transport/test/index.test.ts b/packages/pino-transport/test/index.test.ts
deleted file mode 100644
index a93d56f340cd..000000000000
--- a/packages/pino-transport/test/index.test.ts
+++ /dev/null
@@ -1,708 +0,0 @@
-import { _INTERNAL_captureLog } from '@sentry/core';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { createSentryPinoTransport } from '../src';
-
-// Mock the _INTERNAL_captureLog function
-vi.mock('@sentry/core', async actual => {
- const actualModule = (await actual()) as any;
- return {
- ...actualModule,
- _INTERNAL_captureLog: vi.fn(),
- };
-});
-
-const mockCaptureLog = vi.mocked(_INTERNAL_captureLog);
-
-describe('createSentryPinoTransport', () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- afterEach(() => {
- vi.resetAllMocks();
- });
-
- it('should be defined', () => {
- expect(createSentryPinoTransport).toBeDefined();
- });
-
- it('should create a transport that forwards logs to Sentry', async () => {
- const transport = await createSentryPinoTransport();
- expect(transport).toBeDefined();
- expect(typeof transport.write).toBe('function');
- });
-
- it('should capture logs with correct level mapping', async () => {
- const transport = await createSentryPinoTransport();
-
- // Simulate a Pino log entry
- const testLog = {
- level: 30, // info level in Pino
- msg: 'Test message',
- time: Date.now(),
- hostname: 'test-host',
- pid: 12345,
- };
-
- // Write the log to the transport
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- // Give it a moment to process
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'Test message',
- attributes: expect.objectContaining({
- hostname: 'test-host',
- pid: 12345,
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should map all Pino log levels correctly', async () => {
- const transport = await createSentryPinoTransport();
-
- const testCases = [
- { pinoLevel: 10, expectedSentryLevel: 'trace' },
- { pinoLevel: 20, expectedSentryLevel: 'debug' },
- { pinoLevel: 30, expectedSentryLevel: 'info' },
- { pinoLevel: 40, expectedSentryLevel: 'warn' },
- { pinoLevel: 50, expectedSentryLevel: 'error' },
- { pinoLevel: 60, expectedSentryLevel: 'fatal' },
- ];
-
- for (const { pinoLevel, expectedSentryLevel } of testCases) {
- const testLog = {
- level: pinoLevel,
- msg: `Test ${expectedSentryLevel} message`,
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
- }
-
- // Give it a moment to process all logs
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledTimes(6);
-
- testCases.forEach(({ expectedSentryLevel }, index) => {
- expect(mockCaptureLog).toHaveBeenNthCalledWith(index + 1, {
- level: expectedSentryLevel,
- message: `Test ${expectedSentryLevel} message`,
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
- });
-
- it('should respect level filtering', async () => {
- const transport = await createSentryPinoTransport({
- logLevels: ['error', 'fatal'],
- });
-
- const testLogs = [
- { level: 30, msg: 'Info message' }, // Should be filtered out
- { level: 50, msg: 'Error message' }, // Should be captured
- { level: 60, msg: 'Fatal message' }, // Should be captured
- ];
-
- for (const testLog of testLogs) {
- transport.write(`${JSON.stringify(testLog)}\n`);
- }
-
- // Give it a moment to process all logs
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledTimes(2);
- expect(mockCaptureLog).toHaveBeenNthCalledWith(
- 1,
- expect.objectContaining({
- level: 'error',
- message: 'Error message',
- }),
- );
- expect(mockCaptureLog).toHaveBeenNthCalledWith(
- 2,
- expect.objectContaining({
- level: 'fatal',
- message: 'Fatal message',
- }),
- );
- });
-
- it('should handle unknown levels gracefully', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 999, // Unknown level
- msg: 'Unknown level message',
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- // Give it a moment to process
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'fatal', // 999 maps to fatal (55+ range)
- message: 'Unknown level message',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle non-numeric levels gracefully', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 'invalid', // Non-numeric level
- msg: 'Invalid level message',
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- // Give it a moment to process
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info', // Default fallback
- message: 'Invalid level message',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- 'sentry.pino.level': 'invalid',
- }),
- });
- });
-
- it('should handle malformed JSON gracefully', async () => {
- const transport = await createSentryPinoTransport();
-
- // Write invalid JSON
- transport.write('{ invalid json \n');
-
- // Give it a moment to process
- await new Promise(resolve => setTimeout(resolve, 10));
-
- // Should not crash and should not call captureLog
- expect(mockCaptureLog).not.toHaveBeenCalled();
- });
-
- it('should handle non-object logs gracefully', async () => {
- const transport = await createSentryPinoTransport();
-
- // Write a string instead of an object
- transport.write('"just a string"\n');
-
- // Give it a moment to process
- await new Promise(resolve => setTimeout(resolve, 10));
-
- // pino-abstract-transport parses JSON, so this actually becomes an object
- // The transport should handle it gracefully by logging it
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info', // Default fallback since no level provided
- message: '', // Empty string for undefined message
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle string levels gracefully when no custom levels config is available', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 'custom', // String level without custom levels config
- msg: 'Custom string level message',
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info', // Should fallback to info for unknown string levels
- message: 'Custom string level message',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- 'sentry.pino.level': 'custom',
- }),
- });
- });
-
- it('should attach custom level name as attribute for string levels', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 'critical', // Custom string level
- msg: 'Critical level message',
- time: Date.now(),
- userId: 123,
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info', // Mapped level
- message: 'Critical level message',
- attributes: expect.objectContaining({
- userId: 123,
- 'sentry.origin': 'auto.logging.pino',
- 'sentry.pino.level': 'critical', // Original custom level name preserved
- }),
- });
- });
-
- it('should not attach custom level attribute for numeric levels', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30, // Standard numeric level
- msg: 'Standard level message',
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'Standard level message',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- // Should NOT have 'sentry.pino.level' for numeric levels
- }),
- });
-
- // Explicitly check that the custom level attribute is not present
- const capturedCall = mockCaptureLog.mock.calls[0][0];
- expect(capturedCall.attributes).not.toHaveProperty('sentry.pino.level');
- });
-
- it('should handle custom numeric levels with range-based mapping', async () => {
- const transport = await createSentryPinoTransport();
-
- const testCases = [
- { level: 11, expectedSentryLevel: 'trace' }, // 11 is in trace range (0-14)
- { level: 23, expectedSentryLevel: 'debug' }, // 23 is in debug range (15-24)
- { level: 33, expectedSentryLevel: 'info' }, // 33 is in info range (25-34)
- { level: 42, expectedSentryLevel: 'warn' }, // 42 is in warn range (35-44)
- { level: 52, expectedSentryLevel: 'error' }, // 52 is in error range (45-54)
- { level: 75, expectedSentryLevel: 'fatal' }, // 75 is in fatal range (55+)
- ];
-
- for (const { level } of testCases) {
- const testLog = {
- level,
- msg: `Custom numeric level ${level}`,
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
- }
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledTimes(6);
-
- testCases.forEach(({ level, expectedSentryLevel }, index) => {
- expect(mockCaptureLog).toHaveBeenNthCalledWith(index + 1, {
- level: expectedSentryLevel,
- message: `Custom numeric level ${level}`,
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
- });
-
- it('should handle nested keys', async () => {
- const transport = await createSentryPinoTransport();
-
- // Test with logs that include a nested object structure as Pino would create
- // when nestedKey is configured (we'll test by manually checking the flattening logic)
- const testLog = {
- level: 30,
- msg: 'Test message with nested payload',
- time: Date.now(),
- payload: {
- level: 'hi', // Conflicting with Pino's level
- time: 'never', // Conflicting with Pino's time
- foo: 'bar',
- userId: 123,
- },
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- // Without nestedKey configuration, the nested object should remain as-is
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'Test message with nested payload',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- payload: {
- level: 'hi',
- time: 'never',
- foo: 'bar',
- userId: 123,
- }, // Should remain nested without nestedKey config
- }),
- });
- });
-
- it('should handle logs without conflicting nested objects', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 40,
- msg: 'Warning with simple nested data',
- time: Date.now(),
- data: {
- errorCode: 'E001',
- module: 'auth',
- details: 'Invalid credentials',
- },
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'warn',
- message: 'Warning with simple nested data',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- data: {
- errorCode: 'E001',
- module: 'auth',
- details: 'Invalid credentials',
- }, // Should remain as nested object
- }),
- });
- });
-
- it('should handle logs with multiple nested objects', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: 'Test message with multiple nested objects',
- time: Date.now(),
- user: {
- id: 123,
- name: 'John Doe',
- },
- request: {
- method: 'POST',
- url: '/api/users',
- headers: {
- 'content-type': 'application/json',
- },
- },
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'Test message with multiple nested objects',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- user: {
- id: 123,
- name: 'John Doe',
- },
- request: {
- method: 'POST',
- url: '/api/users',
- headers: {
- 'content-type': 'application/json',
- },
- },
- }),
- });
- });
-
- it('should handle null nested objects', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: 'Test message with null values',
- time: Date.now(),
- data: null,
- user: undefined,
- config: {
- setting: null,
- },
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'Test message with null values',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- data: null,
- config: {
- setting: null,
- },
- }),
- });
- });
-
- it('should work normally with mixed data types', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: 'Mixed data types log',
- time: Date.now(),
- stringValue: 'test',
- numberValue: 42,
- booleanValue: true,
- arrayValue: [1, 2, 3],
- objectValue: { nested: 'value' },
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'Mixed data types log',
- attributes: expect.objectContaining({
- stringValue: 'test',
- numberValue: 42,
- booleanValue: true,
- arrayValue: [1, 2, 3],
- objectValue: { nested: 'value' },
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle string messages', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: 'This is a string message',
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: 'This is a string message',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle number messages', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: 42,
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: '42',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle boolean messages', async () => {
- const transport = await createSentryPinoTransport();
-
- const testCases = [{ msg: true }, { msg: false }];
-
- for (const { msg } of testCases) {
- const testLog = {
- level: 30,
- msg,
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
- }
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledTimes(2);
- expect(mockCaptureLog).toHaveBeenNthCalledWith(1, {
- level: 'info',
- message: 'true',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- expect(mockCaptureLog).toHaveBeenNthCalledWith(2, {
- level: 'info',
- message: 'false',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle null and undefined messages', async () => {
- const transport = await createSentryPinoTransport();
-
- const testCases = [{ msg: null }, { msg: undefined }];
-
- for (const { msg } of testCases) {
- const testLog = {
- level: 30,
- msg,
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
- }
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledTimes(2);
- expect(mockCaptureLog).toHaveBeenNthCalledWith(1, {
- level: 'info',
- message: 'null',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- expect(mockCaptureLog).toHaveBeenNthCalledWith(2, {
- level: 'info',
- message: '',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle object messages', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: { key: 'value', nested: { prop: 123 } },
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: '{"key":"value","nested":{"prop":123}}',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle array messages', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- msg: [1, 'two', { three: 3 }],
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: '[1,"two",{"three":3}]',
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle circular object messages gracefully', async () => {
- const transport = await createSentryPinoTransport();
-
- // Create a test log with a circular object as the message
- // We can't use JSON.stringify directly, so we'll simulate what happens
- const testLog = {
- level: 30,
- msg: { name: 'test', circular: true }, // Simplified object that represents circular data
- time: Date.now(),
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: '{"name":"test","circular":true}', // The object should be serialized normally
- attributes: expect.objectContaining({
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-
- it('should handle missing message gracefully', async () => {
- const transport = await createSentryPinoTransport();
-
- const testLog = {
- level: 30,
- // No msg property
- time: Date.now(),
- someOtherData: 'value',
- };
-
- transport.write(`${JSON.stringify(testLog)}\n`);
-
- await new Promise(resolve => setTimeout(resolve, 10));
-
- expect(mockCaptureLog).toHaveBeenCalledWith({
- level: 'info',
- message: '', // Empty string for undefined message
- attributes: expect.objectContaining({
- someOtherData: 'value',
- 'sentry.origin': 'auto.logging.pino',
- }),
- });
- });
-});
diff --git a/packages/pino-transport/tsconfig.json b/packages/pino-transport/tsconfig.json
deleted file mode 100644
index 64d6f3a1b9e0..000000000000
--- a/packages/pino-transport/tsconfig.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "extends": "../../tsconfig.json",
-
- "include": ["src/**/*"],
-
- "compilerOptions": {
- "lib": ["es2020"],
- "module": "Node16"
- }
-}
diff --git a/packages/pino-transport/tsconfig.test.json b/packages/pino-transport/tsconfig.test.json
deleted file mode 100644
index 4c24dbbea96e..000000000000
--- a/packages/pino-transport/tsconfig.test.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "include": ["test/**/*", "src/**/*", "vite.config.ts"],
- "compilerOptions": {
- "types": ["vitest/globals", "node"]
- }
-}
diff --git a/packages/pino-transport/tsconfig.types.json b/packages/pino-transport/tsconfig.types.json
deleted file mode 100644
index f35cdd6b5d81..000000000000
--- a/packages/pino-transport/tsconfig.types.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "outDir": "build/types",
- "declaration": true,
- "declarationMap": true,
- "emitDeclarationOnly": true,
- "stripInternal": true
- }
-}
diff --git a/packages/pino-transport/vite.config.ts b/packages/pino-transport/vite.config.ts
deleted file mode 100644
index ff64487a9265..000000000000
--- a/packages/pino-transport/vite.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { defineConfig } from 'vitest/config';
-import baseConfig from '../../vite/vite.config';
-
-export default defineConfig({
- ...baseConfig,
- test: {
- ...baseConfig.test,
- environment: 'node',
- },
-});
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index 1b41ba966a1b..328551df5fd1 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -50,7 +50,7 @@
"@opentelemetry/instrumentation": "^0.204.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@sentry/browser": "10.17.0",
- "@sentry/cli": "^2.53.0",
+ "@sentry/cli": "^2.56.0",
"@sentry/core": "10.17.0",
"@sentry/node": "10.17.0",
"@sentry/react": "10.17.0",
diff --git a/packages/react-router/src/server/createSentryHandleRequest.tsx b/packages/react-router/src/server/createSentryHandleRequest.tsx
index eaf13eb16779..75080b827165 100644
--- a/packages/react-router/src/server/createSentryHandleRequest.tsx
+++ b/packages/react-router/src/server/createSentryHandleRequest.tsx
@@ -53,6 +53,22 @@ export interface SentryHandleRequestOptions {
botRegex?: RegExp;
}
+type HandleRequestWithoutMiddleware = (
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ loadContext: AppLoadContext,
+) => Promise;
+
+type HandleRequestWithMiddleware = (
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ loadContext: RouterContextProvider,
+) => Promise;
+
/**
* A complete Sentry-instrumented handleRequest implementation that handles both
* route parametrization and trace meta tag injection.
@@ -62,13 +78,7 @@ export interface SentryHandleRequestOptions {
*/
export function createSentryHandleRequest(
options: SentryHandleRequestOptions,
-): (
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- routerContext: EntryContext,
- loadContext: AppLoadContext | RouterContextProvider,
-) => Promise {
+): HandleRequestWithoutMiddleware & HandleRequestWithMiddleware {
const {
streamTimeout = 10000,
renderToPipeableStream,
@@ -135,5 +145,6 @@ export function createSentryHandleRequest(
};
// Wrap the handle request function for request parametrization
- return wrapSentryHandleRequest(handleRequest);
+ return wrapSentryHandleRequest(handleRequest as HandleRequestWithoutMiddleware) as HandleRequestWithoutMiddleware &
+ HandleRequestWithMiddleware;
}
diff --git a/packages/react-router/src/server/wrapSentryHandleRequest.ts b/packages/react-router/src/server/wrapSentryHandleRequest.ts
index 5651ad208a9d..308b5c07a32b 100644
--- a/packages/react-router/src/server/wrapSentryHandleRequest.ts
+++ b/packages/react-router/src/server/wrapSentryHandleRequest.ts
@@ -10,12 +10,20 @@ import {
} from '@sentry/core';
import type { AppLoadContext, EntryContext, RouterContextProvider } from 'react-router';
-type OriginalHandleRequest = (
+type OriginalHandleRequestWithoutMiddleware = (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
- loadContext: AppLoadContext | RouterContextProvider,
+ loadContext: AppLoadContext,
+) => Promise;
+
+type OriginalHandleRequestWithMiddleware = (
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ loadContext: RouterContextProvider,
) => Promise;
/**
@@ -24,7 +32,27 @@ type OriginalHandleRequest = (
* @param originalHandle - The original handleRequest function to wrap
* @returns A wrapped version of the handle request function with Sentry instrumentation
*/
-export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest): OriginalHandleRequest {
+export function wrapSentryHandleRequest(
+ originalHandle: OriginalHandleRequestWithoutMiddleware,
+): OriginalHandleRequestWithoutMiddleware;
+/**
+ * Wraps the original handleRequest function to add Sentry instrumentation.
+ *
+ * @param originalHandle - The original handleRequest function to wrap
+ * @returns A wrapped version of the handle request function with Sentry instrumentation
+ */
+export function wrapSentryHandleRequest(
+ originalHandle: OriginalHandleRequestWithMiddleware,
+): OriginalHandleRequestWithMiddleware;
+/**
+ * Wraps the original handleRequest function to add Sentry instrumentation.
+ *
+ * @param originalHandle - The original handleRequest function to wrap
+ * @returns A wrapped version of the handle request function with Sentry instrumentation
+ */
+export function wrapSentryHandleRequest(
+ originalHandle: OriginalHandleRequestWithoutMiddleware | OriginalHandleRequestWithMiddleware,
+): OriginalHandleRequestWithoutMiddleware | OriginalHandleRequestWithMiddleware {
return async function sentryInstrumentedHandleRequest(
request: Request,
responseStatusCode: number,
@@ -57,10 +85,39 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest):
}
try {
- return await originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext);
+ // Type guard to call the correct overload based on loadContext type
+ if (isRouterContextProvider(loadContext)) {
+ // loadContext is RouterContextProvider
+ return await (originalHandle as OriginalHandleRequestWithMiddleware)(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ routerContext,
+ loadContext,
+ );
+ } else {
+ // loadContext is AppLoadContext
+ return await (originalHandle as OriginalHandleRequestWithoutMiddleware)(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ routerContext,
+ loadContext,
+ );
+ }
} finally {
await flushIfServerless();
}
+
+ /**
+ * Helper type guard to determine if the context is a RouterContextProvider.
+ *
+ * @param ctx - The context to check
+ * @returns True if the context is a RouterContextProvider
+ */
+ function isRouterContextProvider(ctx: AppLoadContext | RouterContextProvider): ctx is RouterContextProvider {
+ return typeof (ctx as RouterContextProvider)?.get === 'function';
+ }
};
}
diff --git a/packages/remix/package.json b/packages/remix/package.json
index 67c687ef0405..66c1b859565a 100644
--- a/packages/remix/package.json
+++ b/packages/remix/package.json
@@ -68,7 +68,7 @@
"@opentelemetry/instrumentation": "^0.204.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@remix-run/router": "1.x",
- "@sentry/cli": "^2.53.0",
+ "@sentry/cli": "^2.56.0",
"@sentry/core": "10.17.0",
"@sentry/node": "10.17.0",
"@sentry/react": "10.17.0",
diff --git a/yarn.lock b/yarn.lock
index 8c030924b033..4d084a8cf3c7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -335,6 +335,20 @@
dependencies:
json-schema-to-ts "^3.1.1"
+"@apm-js-collab/code-transformer@^0.8.0", "@apm-js-collab/code-transformer@^0.8.2":
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/@apm-js-collab/code-transformer/-/code-transformer-0.8.2.tgz#a3160f16d1c4df9cb81303527287ad18d00994d1"
+ integrity sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA==
+
+"@apm-js-collab/tracing-hooks@^0.3.1":
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.3.1.tgz#414d3a93c3a15d8be543a3fac561f7c602b6a588"
+ integrity sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==
+ dependencies:
+ "@apm-js-collab/code-transformer" "^0.8.0"
+ debug "^4.4.1"
+ module-details-from-path "^1.0.4"
+
"@apollo/protobufjs@1.2.6":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.6.tgz#d601e65211e06ae1432bf5993a1a0105f2862f27"
@@ -4958,26 +4972,6 @@
dependencies:
sparse-bitfield "^3.0.3"
-"@nestjs/common@11.0.16":
- version "11.0.16"
- resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.0.16.tgz#b6550ac2998e9991f24a99563a93475542885ba7"
- integrity sha512-agvuQ8su4aZ+PVxAmY89odG1eR97HEQvxPmTMdDqyvDWzNerl7WQhUEd+j4/UyNWcF1or1UVcrtPj52x+eUSsA==
- dependencies:
- uid "2.0.2"
- iterare "1.2.1"
- tslib "2.8.1"
-
-"@nestjs/common@11.1.3":
- version "11.1.3"
- resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.3.tgz#d954644da5f4d1b601e48ee71a0d3e3405d81ea1"
- integrity sha512-ogEK+GriWodIwCw6buQ1rpcH4Kx+G7YQ9EwuPySI3rS05pSdtQ++UhucjusSI9apNidv+QURBztJkRecwwJQXg==
- dependencies:
- uid "2.0.2"
- file-type "21.0.0"
- iterare "1.2.1"
- load-esm "1.0.2"
- tslib "2.8.1"
-
"@nestjs/common@^10.0.0":
version "10.4.15"
resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.15.tgz#27c291466d9100eb86fdbe6f7bbb4d1a6ad55f70"
@@ -4987,28 +4981,15 @@
iterare "1.2.1"
tslib "2.8.1"
-"@nestjs/core@10.4.6":
- version "10.4.6"
- resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.6.tgz#797b381f12bd62d2e425897058fa219da4c3689d"
- integrity sha512-zXVPxCNRfO6gAy0yvEDjUxE/8gfZICJFpsl2lZAUH31bPb6m+tXuhUq2mVCTEltyMYQ+DYtRe+fEYM2v152N1g==
- dependencies:
- uid "2.0.2"
- "@nuxtjs/opencollective" "0.3.2"
- fast-safe-stringify "2.1.1"
- iterare "1.2.1"
- path-to-regexp "3.3.0"
- tslib "2.7.0"
-
-"@nestjs/core@11.1.3":
- version "11.1.3"
- resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-11.1.3.tgz#42a9c6261ff70ef49afa809c526134cae22021e8"
- integrity sha512-5lTni0TCh8x7bXETRD57pQFnKnEg1T6M+VLE7wAmyQRIecKQU+2inRGZD+A4v2DC1I04eA0WffP0GKLxjOKlzw==
+"@nestjs/common@^11":
+ version "11.1.6"
+ resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.6.tgz#704ae26f09ccd135bf3e6f44b6ef4e3407ea3c54"
+ integrity sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==
dependencies:
uid "2.0.2"
- "@nuxt/opencollective" "0.4.1"
- fast-safe-stringify "2.1.1"
+ file-type "21.0.0"
iterare "1.2.1"
- path-to-regexp "8.2.0"
+ load-esm "1.0.2"
tslib "2.8.1"
"@nestjs/core@^10.0.0":
@@ -5023,25 +5004,26 @@
path-to-regexp "3.3.0"
tslib "2.8.1"
-"@nestjs/platform-express@10.4.6":
- version "10.4.6"
- resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de"
- integrity sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg==
+"@nestjs/core@^11":
+ version "11.1.6"
+ resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-11.1.6.tgz#9d54882f121168b2fa2b07fa1db0858161a80626"
+ integrity sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==
dependencies:
- body-parser "1.20.3"
- cors "2.8.5"
- express "4.21.1"
- multer "1.4.4-lts.1"
- tslib "2.7.0"
+ uid "2.0.2"
+ "@nuxt/opencollective" "0.4.1"
+ fast-safe-stringify "2.1.1"
+ iterare "1.2.1"
+ path-to-regexp "8.2.0"
+ tslib "2.8.1"
-"@nestjs/platform-express@11.1.3":
- version "11.1.3"
- resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-11.1.3.tgz#bf470f2e270ca9daa930974476dd0d7d62879556"
- integrity sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==
+"@nestjs/platform-express@^11":
+ version "11.1.6"
+ resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-11.1.6.tgz#9b1dcf82a3b3fdd5761c918ad664aff83e4eacc7"
+ integrity sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==
dependencies:
cors "2.8.5"
express "5.1.0"
- multer "2.0.1"
+ multer "2.0.2"
path-to-regexp "8.2.0"
tslib "2.8.1"
@@ -7007,50 +6989,50 @@
magic-string "0.30.8"
unplugin "1.0.1"
-"@sentry/cli-darwin@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.53.0.tgz#0584f5a4a376c9373f91ad5e1d9194278be2aed6"
- integrity sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==
-
-"@sentry/cli-linux-arm64@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.53.0.tgz#04a73b2592edf10d6e06957905becc98692605b1"
- integrity sha512-xY/CZ1dVazsSCvTXzKpAgXaRqfljVfdrFaYZRUaRPf1ZJRGa3dcrivoOhSIeG/p5NdYtMvslMPY9Gm2MT0M83A==
-
-"@sentry/cli-linux-arm@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.53.0.tgz#caa1dceb23ee40e9d0c82a7c6156c3f010eebc0e"
- integrity sha512-NdRzQ15Ht83qG0/Lyu11ciy/Hu/oXbbtJUgwzACc7bWvHQA8xEwTsehWexqn1529Kfc5EjuZ0Wmj3MHmp+jOWw==
-
-"@sentry/cli-linux-i686@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.53.0.tgz#989dc766b098e94c6751bad3efcd4ca0fe1a2565"
- integrity sha512-0REmBibGAB4jtqt9S6JEsFF4QybzcXHPcHtJjgMi5T0ueh952uG9wLzjSxQErCsxTKF+fL8oG0Oz5yKBuCwCCQ==
-
-"@sentry/cli-linux-x64@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.53.0.tgz#2a94361233ed24e4a32f08919011a591aea4cb6b"
- integrity sha512-9UGJL+Vy5N/YL1EWPZ/dyXLkShlNaDNrzxx4G7mTS9ywjg+BIuemo6rnN7w43K1NOjObTVO6zY0FwumJ1pCyLg==
-
-"@sentry/cli-win32-arm64@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.53.0.tgz#946609eabd318657521c4b3ef15a420cc00f1c60"
- integrity sha512-G1kjOjrjMBY20rQcJV2GA8KQE74ufmROCDb2GXYRfjvb1fKAsm4Oh8N5+Tqi7xEHdjQoLPkE4CNW0aH68JSUDQ==
-
-"@sentry/cli-win32-i686@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.53.0.tgz#f51937d73cefad16b9d2e89acc4c9f178da36cc6"
- integrity sha512-qbGTZUzesuUaPtY9rPXdNfwLqOZKXrJRC1zUFn52hdo6B+Dmv0m/AHwRVFHZP53Tg1NCa8bDei2K/uzRN0dUZw==
-
-"@sentry/cli-win32-x64@2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.53.0.tgz#d89cde8354b4eb8e89f2c11dc6a6fb5e7392e2ae"
- integrity sha512-1TXYxYHtwgUq5KAJt3erRzzUtPqg7BlH9T7MdSPHjJatkrr/kwZqnVe2H6Arr/5NH891vOlIeSPHBdgJUAD69g==
-
-"@sentry/cli@^2.51.0", "@sentry/cli@^2.53.0":
- version "2.53.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.53.0.tgz#fd5b65b9f6f06f0ed16345acf3ecf0720bd7bcf8"
- integrity sha512-n2ZNb+5Z6AZKQSI0SusQ7ZzFL637mfw3Xh4C3PEyVSn9LiF683fX0TTq8OeGmNZQS4maYfS95IFD+XpydU0dEA==
+"@sentry/cli-darwin@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.56.0.tgz#53fa7de2c26f6450d5454ba997c26c2471d112c8"
+ integrity sha512-CzXFWbv3GrjU0gFlUM9jt0fvJmyo5ktty4HGxRFfS/eMC6xW58Gg/sEeMVEkdvk5osKooX/YEgfLBdo4zvuWDA==
+
+"@sentry/cli-linux-arm64@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.56.0.tgz#5041c8877416a607ddae87b948cbe6c9e86d7f54"
+ integrity sha512-91d5ZlC989j/t+TXor/glPyx6SnLFS/SlJ9fIrHIQohdGKyWWSFb4VKUan8Ok3GYu9SUzKTMByryIOoYEmeGVw==
+
+"@sentry/cli-linux-arm@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.56.0.tgz#c7875cf5f76e254ff1c0f49cf99d8c26b6ec4959"
+ integrity sha512-vQCCMhZLugPmr25XBoP94dpQsFa110qK5SBUVJcRpJKyzMZd+6ueeHNslq2mB0OF4BwL1qd/ZDIa4nxa1+0rjQ==
+
+"@sentry/cli-linux-i686@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.56.0.tgz#aeaff32f9f0d405e413373223e406d66b1d56176"
+ integrity sha512-MZzXuq1Q/TktN81DUs6XSBU752pG3XWSJdZR+NCStIg3l8s3O/Pwh6OcDHTYqgwsYJaGBpA0fP2Afl5XeSAUNg==
+
+"@sentry/cli-linux-x64@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.56.0.tgz#3dd4ef83c2d710c3e6f5d078d05391fda2ce23ee"
+ integrity sha512-INOO2OQ90Y3UzYgHRdrHdKC/0es3YSHLv0iNNgQwllL0YZihSVNYSSrZqcPq8oSDllEy9Vt9oOm/7qEnUP2Kfw==
+
+"@sentry/cli-win32-arm64@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.56.0.tgz#2113bcac721970ca4dbd04a6dab37dfb0ec147d2"
+ integrity sha512-eUvkVk9KK01q6/qyugQPh7dAxqFPbgOa62QAoSwo11WQFYc3NPgJLilFWLQo+nahHGYKh6PKuCJ5tcqnQq5Hkg==
+
+"@sentry/cli-win32-i686@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.56.0.tgz#bd8e646f4b5a98aa80bc9751a6e0db6514a935f5"
+ integrity sha512-mpCA8hKXuvT17bl1H/54KOa5i+02VBBHVlOiP3ltyBuQUqfvX/30Zl/86Spy+ikodovZWAHv5e5FpyXbY1/mPw==
+
+"@sentry/cli-win32-x64@2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.56.0.tgz#1acc7ca166ed531075a31b2bc1700294747da6b8"
+ integrity sha512-UV0pXNls+/ViAU/3XsHLLNEHCsRYaGEwJdY3HyGIufSlglxrX6BVApkV9ziGi4WAxcJWLjQdfcEs6V5B+wBy0A==
+
+"@sentry/cli@^2.51.0", "@sentry/cli@^2.56.0":
+ version "2.56.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.56.0.tgz#13dc043c78687b47285cc45db5bcfb65bbdb6dd9"
+ integrity sha512-br6+1nTPUV5EG1oaxLzxv31kREFKr49Y1+3jutfMUz9Nl8VyVP7o9YwakB/YWl+0Vi0NXg5vq7qsd/OOuV5j8w==
dependencies:
https-proxy-agent "^5.0.0"
node-fetch "^2.6.7"
@@ -7058,14 +7040,14 @@
proxy-from-env "^1.1.0"
which "^2.0.2"
optionalDependencies:
- "@sentry/cli-darwin" "2.53.0"
- "@sentry/cli-linux-arm" "2.53.0"
- "@sentry/cli-linux-arm64" "2.53.0"
- "@sentry/cli-linux-i686" "2.53.0"
- "@sentry/cli-linux-x64" "2.53.0"
- "@sentry/cli-win32-arm64" "2.53.0"
- "@sentry/cli-win32-i686" "2.53.0"
- "@sentry/cli-win32-x64" "2.53.0"
+ "@sentry/cli-darwin" "2.56.0"
+ "@sentry/cli-linux-arm" "2.56.0"
+ "@sentry/cli-linux-arm64" "2.56.0"
+ "@sentry/cli-linux-i686" "2.56.0"
+ "@sentry/cli-linux-x64" "2.56.0"
+ "@sentry/cli-win32-arm64" "2.56.0"
+ "@sentry/cli-win32-i686" "2.56.0"
+ "@sentry/cli-win32-x64" "2.56.0"
"@sentry/rollup-plugin@^4.3.0":
version "4.3.0"
@@ -10956,13 +10938,13 @@ aws-ssl-profiles@^1.1.1:
resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641"
integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==
-axios@1.8.2, axios@^1.0.0:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
- integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
+axios@^1.0.0, axios@^1.12.2:
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
+ integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
dependencies:
follow-redirects "^1.15.6"
- form-data "^4.0.0"
+ form-data "^4.0.4"
proxy-from-env "^1.1.0"
axobject-query@^3.2.1:
@@ -12132,7 +12114,7 @@ bundle-name@^4.1.0:
dependencies:
run-applescript "^7.0.0"
-busboy@1.6.0, busboy@^1.0.0, busboy@^1.6.0:
+busboy@1.6.0, busboy@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
@@ -13052,16 +13034,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-concat-stream@^1.5.2:
- version "1.6.2"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
- integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
- dependencies:
- buffer-from "^1.0.0"
- inherits "^2.0.3"
- readable-stream "^2.2.2"
- typedarray "^0.0.6"
-
concat-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
@@ -16512,7 +16484,40 @@ expect-type@^1.2.1:
resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.1.tgz#af76d8b357cf5fa76c41c09dafb79c549e75f71f"
integrity sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==
-express@4.21.1, express@^4.10.7, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.21.1:
+express@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9"
+ integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==
+ dependencies:
+ accepts "^2.0.0"
+ body-parser "^2.2.0"
+ content-disposition "^1.0.0"
+ content-type "^1.0.5"
+ cookie "^0.7.1"
+ cookie-signature "^1.2.1"
+ debug "^4.4.0"
+ encodeurl "^2.0.0"
+ escape-html "^1.0.3"
+ etag "^1.8.1"
+ finalhandler "^2.1.0"
+ fresh "^2.0.0"
+ http-errors "^2.0.0"
+ merge-descriptors "^2.0.0"
+ mime-types "^3.0.0"
+ on-finished "^2.4.1"
+ once "^1.4.0"
+ parseurl "^1.3.3"
+ proxy-addr "^2.0.7"
+ qs "^6.14.0"
+ range-parser "^1.2.1"
+ router "^2.2.0"
+ send "^1.1.0"
+ serve-static "^2.2.0"
+ statuses "^2.0.1"
+ type-is "^2.0.1"
+ vary "^1.1.2"
+
+express@^4.10.7, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.21.1:
version "4.21.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281"
integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==
@@ -16549,39 +16554,6 @@ express@4.21.1, express@^4.10.7, express@^4.17.1, express@^4.17.3, express@^4.18
utils-merge "1.0.1"
vary "~1.1.2"
-express@5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9"
- integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==
- dependencies:
- accepts "^2.0.0"
- body-parser "^2.2.0"
- content-disposition "^1.0.0"
- content-type "^1.0.5"
- cookie "^0.7.1"
- cookie-signature "^1.2.1"
- debug "^4.4.0"
- encodeurl "^2.0.0"
- escape-html "^1.0.3"
- etag "^1.8.1"
- finalhandler "^2.1.0"
- fresh "^2.0.0"
- http-errors "^2.0.0"
- merge-descriptors "^2.0.0"
- mime-types "^3.0.0"
- on-finished "^2.4.1"
- once "^1.4.0"
- parseurl "^1.3.3"
- proxy-addr "^2.0.7"
- qs "^6.14.0"
- range-parser "^1.2.1"
- router "^2.2.0"
- send "^1.1.0"
- serve-static "^2.2.0"
- statuses "^2.0.1"
- type-is "^2.0.1"
- vary "^1.1.2"
-
exsolve@^1.0.4, exsolve@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.7.tgz#3b74e4c7ca5c5f9a19c3626ca857309fa99f9e9e"
@@ -17213,7 +17185,7 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.6"
signal-exit "^4.0.1"
-form-data@^4.0.0:
+form-data@^4.0.0, form-data@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
@@ -22423,7 +22395,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@^0.5.6:
+mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
@@ -22495,10 +22467,10 @@ module-definition@^6.0.1:
ast-module-types "^6.0.1"
node-source-walk "^7.0.1"
-module-details-from-path@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b"
- integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==
+module-details-from-path@^1.0.3, module-details-from-path@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz#b662fdcd93f6c83d3f25289da0ce81c8d9685b94"
+ integrity sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==
module-lookup-amd@^8.0.5:
version "8.0.5"
@@ -22663,23 +22635,10 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-multer@1.4.4-lts.1:
- version "1.4.4-lts.1"
- resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d"
- integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==
- dependencies:
- append-field "^1.0.0"
- busboy "^1.0.0"
- concat-stream "^1.5.2"
- mkdirp "^0.5.4"
- object-assign "^4.1.1"
- type-is "^1.6.4"
- xtend "^4.0.0"
-
-multer@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/multer/-/multer-2.0.1.tgz#3ed335ed2b96240e3df9e23780c91cfcf5d29202"
- integrity sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==
+multer@2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/multer/-/multer-2.0.2.tgz#08a8aa8255865388c387aaf041426b0c87bf58dd"
+ integrity sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==
dependencies:
append-field "^1.0.0"
busboy "^1.6.0"
@@ -24796,15 +24755,32 @@ pino-abstract-transport@^2.0.0:
dependencies:
split2 "^4.0.0"
+"pino-next@npm:pino@^9.12.0":
+ version "9.12.0"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-9.12.0.tgz#976e549cc29e21e5dbf56b47910dc52817dc5a97"
+ integrity sha512-0Gd0OezGvqtqMwgYxpL7P0pSHHzTJ0Lx992h+mNlMtRVfNnqweWmf0JmRWk5gJzHalyd2mxTzKjhiNbGS2Ztfw==
+ dependencies:
+ atomic-sleep "^1.0.0"
+ on-exit-leak-free "^2.1.0"
+ pino-abstract-transport "^2.0.0"
+ pino-std-serializers "^7.0.0"
+ process-warning "^5.0.0"
+ quick-format-unescaped "^4.0.3"
+ real-require "^0.2.0"
+ safe-stable-stringify "^2.3.1"
+ slow-redact "^0.3.0"
+ sonic-boom "^4.0.1"
+ thread-stream "^3.0.0"
+
pino-std-serializers@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b"
integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
-pino@^9.0.0:
- version "9.7.0"
- resolved "https://registry.yarnpkg.com/pino/-/pino-9.7.0.tgz#ff7cd86eb3103ee620204dbd5ca6ffda8b53f645"
- integrity sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==
+pino@9.9.4:
+ version "9.9.4"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-9.9.4.tgz#21ed2c27cc177f797e3249c99d340f0bcd6b248e"
+ integrity sha512-d1XorUQ7sSKqVcYdXuEYs2h1LKxejSorMEJ76XoZ0pPDf8VzJMe7GlPXpMBZeQ9gE4ZPIp5uGD+5Nw7scxiigg==
dependencies:
atomic-sleep "^1.0.0"
fast-redact "^3.1.1"
@@ -26329,7 +26305,7 @@ readable-stream@2.3.7:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.3.5, readable-stream@~2.3.6:
+readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.3.5, readable-stream@~2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
@@ -27968,6 +27944,11 @@ sliced@1.0.1:
resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41"
integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==
+slow-redact@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/slow-redact/-/slow-redact-0.3.0.tgz#97b4d7bd04136404e529c1ab29f3cb50e903c746"
+ integrity sha512-cf723wn9JeRIYP9tdtd86GuqoR5937u64Io+CYjlm2i7jvu7g0H+Cp0l0ShAf/4ZL+ISUTVT+8Qzz7RZmp9FjA==
+
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
@@ -28556,7 +28537,7 @@ string-template@~0.2.1:
string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
- resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
@@ -28666,7 +28647,7 @@ stringify-object@^3.2.1:
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
- resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
@@ -29691,11 +29672,6 @@ tslib@2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
-tslib@2.7.0:
- version "2.7.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
- integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
-
tslib@2.8.1, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.2, tslib@^2.6.3, tslib@^2.7.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
@@ -29818,7 +29794,7 @@ type-fest@^4.18.2, type-fest@^4.39.1, type-fest@^4.6.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58"
integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
-type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18:
+type-is@^1.6.18, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -31735,7 +31711,7 @@ wrangler@4.22.0:
wrap-ansi@7.0.0, wrap-ansi@^7.0.0:
version "7.0.0"
- resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"