From f402dd20340eba0a4080c2c5117fd3a285a7d288 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 21 Jul 2025 04:53:05 -0400 Subject: [PATCH 01/14] feat(v9/core): Deprecate experimental `enableLogs` and `beforeSendLog` option (#17092) This is the v9 backport of https://github.com/getsentry/sentry-javascript/pull/17063, which deprecates the `enableLogs` and `beforeSendLog` under experiments and move it to be top-level options. --- MIGRATION.md | 24 ++++++++++ .../suites/public-api/logger/init.js | 4 +- .../public-api/logger/integration/init.js | 4 +- .../test-applications/node-express/src/app.ts | 4 +- .../suites/winston/subject.ts | 4 +- .../suites/winston/subject.ts | 4 +- packages/browser/src/client.ts | 11 +++-- packages/browser/src/log.ts | 12 ++--- packages/browser/test/client.test.ts | 3 +- packages/browser/test/log.test.ts | 4 +- packages/cloudflare/src/logs/exports.ts | 12 ++--- packages/core/src/logs/console-integration.ts | 11 +++-- packages/core/src/logs/exports.ts | 11 +++-- packages/core/src/server-runtime-client.ts | 4 +- packages/core/src/types-hoist/options.ts | 27 +++++++++++ packages/core/test/lib/logs/exports.test.ts | 46 ++++++++++--------- .../test/lib/server-runtime-client.test.ts | 9 ++-- .../node-core/src/integrations/winston.ts | 2 +- packages/node-core/src/logs/exports.ts | 12 ++--- packages/node-core/src/sdk/client.ts | 5 +- packages/node-core/test/sdk/client.test.ts | 4 +- packages/node/test/sdk/client.test.ts | 4 +- packages/pino-transport/README.md | 8 ++-- packages/pino-transport/src/index.ts | 2 +- packages/vercel-edge/src/logs/exports.ts | 12 ++--- 25 files changed, 146 insertions(+), 97 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index ac2a46a8d50e..d3ed9cafc7f0 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -23,6 +23,30 @@ logger.info('This is an info message'); debug.log('This is an info message'); ``` +## Deprecated `_experiments.enableLogs` and `_experiments.beforeSendLog` options + +The `_experiments.enableLogs` and `_experiments.beforeSendLog` options have been deprecated in favor of the top-level `enableLogs` and `beforeSendLog` options. + +```js +// before +Sentry.init({ + _experiments: { + enableLogs: true, + beforeSendLog: log => { + return log; + }, + }, +}); + +// after +Sentry.init({ + enableLogs: true, + beforeSendLog: log => { + return log; + }, +}); +``` + # Upgrading from 8.x to 9.x Version 9 of the Sentry JavaScript SDK primarily introduces API cleanup and version support changes. diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js index 27397e0f90ce..8026df91ea46 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/init.js @@ -4,7 +4,5 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - _experiments: { - enableLogs: true, - }, + enableLogs: true, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js index e0ceaaebf017..809b78739e77 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js @@ -4,8 +4,6 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - _experiments: { - enableLogs: true, - }, + enableLogs: true, integrations: [Sentry.consoleLoggingIntegration()], }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts index 9f7b0055b66d..0ac843b5409e 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts @@ -13,9 +13,7 @@ Sentry.init({ debug: !!process.env.DEBUG, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, - _experiments: { - enableLogs: true, - }, + enableLogs: true, }); import { TRPCError, initTRPC } from '@trpc/server'; diff --git a/dev-packages/node-core-integration-tests/suites/winston/subject.ts b/dev-packages/node-core-integration-tests/suites/winston/subject.ts index c8840b855f9b..3c31ddb63fa5 100644 --- a/dev-packages/node-core-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-core-integration-tests/suites/winston/subject.ts @@ -8,9 +8,7 @@ const client = Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', - _experiments: { - enableLogs: true, - }, + enableLogs: true, transport: loggingTransport, }); diff --git a/dev-packages/node-integration-tests/suites/winston/subject.ts b/dev-packages/node-integration-tests/suites/winston/subject.ts index ac70abc50d80..1047f2f1cd47 100644 --- a/dev-packages/node-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-integration-tests/suites/winston/subject.ts @@ -7,9 +7,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0.0', environment: 'test', - _experiments: { - enableLogs: true, - }, + enableLogs: true, transport: loggingTransport, }); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 5c50e53e708b..be5b7d25caa3 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -85,23 +85,24 @@ export class BrowserClient extends Client { super(opts); - const { sendDefaultPii, sendClientReports, _experiments } = this._options; - const enableLogs = _experiments?.enableLogs; + const { sendDefaultPii, sendClientReports, enableLogs, _experiments } = this._options; + // eslint-disable-next-line deprecation/deprecation + const shouldEnableLogs = enableLogs ?? _experiments?.enableLogs; - if (WINDOW.document && (sendClientReports || enableLogs)) { + if (WINDOW.document && (sendClientReports || shouldEnableLogs)) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { if (sendClientReports) { this._flushOutcomes(); } - if (enableLogs) { + if (shouldEnableLogs) { _INTERNAL_flushLogsBuffer(this); } } }); } - if (enableLogs) { + if (shouldEnableLogs) { this.on('flush', () => { _INTERNAL_flushLogsBuffer(this); }); diff --git a/packages/browser/src/log.ts b/packages/browser/src/log.ts index ef2614b81f55..c21477e378b3 100644 --- a/packages/browser/src/log.ts +++ b/packages/browser/src/log.ts @@ -19,7 +19,7 @@ function captureLog( } /** - * @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `trace` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { userId: 100, route: '/dashboard' }. @@ -48,7 +48,7 @@ export function trace(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `debug` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { component: 'Header', state: 'loading' }. @@ -78,7 +78,7 @@ export function debug(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `info` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { feature: 'checkout', status: 'completed' }. @@ -108,7 +108,7 @@ export function info(message: ParameterizedString, attributes?: Log['attributes' } /** - * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `warn` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { browser: 'Chrome', version: '91.0' }. @@ -139,7 +139,7 @@ export function warn(message: ParameterizedString, attributes?: Log['attributes' } /** - * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `error` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { error: 'NetworkError', url: '/api/data' }. @@ -171,7 +171,7 @@ export function error(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `fatal` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { appState: 'corrupted', sessionId: 'abc-123' }. diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index c0f4e649501a..2197b6ed03c0 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -27,7 +27,6 @@ describe('BrowserClient', () => { it('does not flush logs when logs are disabled', () => { client = new BrowserClient( getDefaultBrowserClientOptions({ - _experiments: { enableLogs: false }, sendClientReports: true, }), ); @@ -50,7 +49,7 @@ describe('BrowserClient', () => { vi.useFakeTimers(); client = new BrowserClient( getDefaultBrowserClientOptions({ - _experiments: { enableLogs: true }, + enableLogs: true, sendClientReports: true, }), ); diff --git a/packages/browser/test/log.test.ts b/packages/browser/test/log.test.ts index 68c87069966c..0967d38531dd 100644 --- a/packages/browser/test/log.test.ts +++ b/packages/browser/test/log.test.ts @@ -42,9 +42,7 @@ describe('Logger', () => { init({ dsn, transport: makeSimpleTransport, - _experiments: { - enableLogs: true, - }, + enableLogs: true, }); }); diff --git a/packages/cloudflare/src/logs/exports.ts b/packages/cloudflare/src/logs/exports.ts index ef2614b81f55..c21477e378b3 100644 --- a/packages/cloudflare/src/logs/exports.ts +++ b/packages/cloudflare/src/logs/exports.ts @@ -19,7 +19,7 @@ function captureLog( } /** - * @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `trace` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { userId: 100, route: '/dashboard' }. @@ -48,7 +48,7 @@ export function trace(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `debug` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { component: 'Header', state: 'loading' }. @@ -78,7 +78,7 @@ export function debug(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `info` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { feature: 'checkout', status: 'completed' }. @@ -108,7 +108,7 @@ export function info(message: ParameterizedString, attributes?: Log['attributes' } /** - * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `warn` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { browser: 'Chrome', version: '91.0' }. @@ -139,7 +139,7 @@ export function warn(message: ParameterizedString, attributes?: Log['attributes' } /** - * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `error` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { error: 'NetworkError', url: '/api/data' }. @@ -171,7 +171,7 @@ export function error(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `fatal` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { appState: 'corrupted', sessionId: 'abc-123' }. diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index d2cdc8fa1d48..18bd6202111a 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -33,9 +33,11 @@ const _consoleLoggingIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup(client) { - const { _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); - if (!_experiments?.enableLogs) { - DEBUG_BUILD && debug.warn('`_experiments.enableLogs` is not enabled, ConsoleLogs integration disabled'); + const { enableLogs, _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); + // eslint-disable-next-line deprecation/deprecation + const shouldEnableLogs = enableLogs ?? _experiments?.enableLogs; + if (!shouldEnableLogs) { + DEBUG_BUILD && debug.warn('`enableLogs` is not enabled, ConsoleLogs integration disabled'); return; } @@ -69,7 +71,7 @@ const _consoleLoggingIntegration = ((options: Partial = { }) satisfies IntegrationFn; /** - * Captures calls to the `console` API as logs in Sentry. Requires `_experiments.enableLogs` to be enabled. + * Captures calls to the `console` API as logs in Sentry. Requires the `enableLogs` option to be enabled. * * @experimental This feature is experimental and may be changed or removed in future versions. * @@ -83,6 +85,7 @@ const _consoleLoggingIntegration = ((options: Partial = { * import * as Sentry from '@sentry/browser'; * * Sentry.init({ + * enableLogs: true, * integrations: [Sentry.consoleLoggingIntegration({ levels: ['error', 'warn'] })], * }); * ``` diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 23246a7e1251..6a921d6f6053 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -124,12 +124,15 @@ export function _INTERNAL_captureLog( return; } - const { _experiments, release, environment } = client.getOptions(); - const { enableLogs = false, beforeSendLog } = _experiments ?? {}; - if (!enableLogs) { + const { release, environment, enableLogs, beforeSendLog, _experiments } = client.getOptions(); + // eslint-disable-next-line deprecation/deprecation + const shouldEnableLogs = enableLogs ?? _experiments?.enableLogs; + if (!shouldEnableLogs) { DEBUG_BUILD && debug.warn('logging option not enabled, log will not be captured.'); return; } + // eslint-disable-next-line deprecation/deprecation + const actualBeforeSendLog = beforeSendLog ?? _experiments?.beforeSendLog; const [, traceContext] = _getTraceInfoFromScope(client, currentScope); @@ -169,7 +172,7 @@ export function _INTERNAL_captureLog( client.emit('beforeCaptureLog', processedLog); // We need to wrap this in `consoleSandbox` to avoid recursive calls to `beforeSendLog` - const log = beforeSendLog ? consoleSandbox(() => beforeSendLog(processedLog)) : processedLog; + const log = actualBeforeSendLog ? consoleSandbox(() => actualBeforeSendLog(processedLog)) : processedLog; if (!log) { client.recordDroppedEvent('before_send', 'log_item', 1); DEBUG_BUILD && debug.warn('beforeSendLog returned null, log will not be captured.'); diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 246e0a88bc51..c63ddbe22e41 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -49,7 +49,9 @@ export class ServerRuntimeClient< this._logWeight = 0; - if (this._options._experiments?.enableLogs) { + // eslint-disable-next-line deprecation/deprecation + const shouldEnableLogs = this._options.enableLogs ?? this._options._experiments?.enableLogs; + if (shouldEnableLogs) { // eslint-disable-next-line @typescript-eslint/no-this-alias const client = this; diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index a4119ed42a6e..488cf9cd70a8 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -246,12 +246,16 @@ export interface ClientOptions Log | null; + /** * Function to compute tracing sample rate dynamically and filter unwanted traces. * diff --git a/packages/core/test/lib/logs/exports.test.ts b/packages/core/test/lib/logs/exports.test.ts index 536ae9a2adfd..4fd01a16a304 100644 --- a/packages/core/test/lib/logs/exports.test.ts +++ b/packages/core/test/lib/logs/exports.test.ts @@ -82,7 +82,7 @@ describe('logAttributeToSerializedLogAttribute', () => { describe('_INTERNAL_captureLog', () => { it('captures and sends logs', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); _INTERNAL_captureLog({ level: 'info', message: 'test log message' }, client, undefined); @@ -99,7 +99,7 @@ describe('_INTERNAL_captureLog', () => { ); }); - it('does not capture logs when enableLogs experiment is not enabled', () => { + it('does not capture logs when enableLogs is not enabled', () => { const logWarnSpy = vi.spyOn(loggerModule.debug, 'warn').mockImplementation(() => undefined); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); @@ -113,7 +113,7 @@ describe('_INTERNAL_captureLog', () => { }); it('includes trace context when available', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); const scope = new Scope(); scope.setPropagationContext({ @@ -134,7 +134,7 @@ describe('_INTERNAL_captureLog', () => { it('includes release and environment in log attributes when available', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, release: '1.0.0', environment: 'test', }); @@ -158,7 +158,7 @@ describe('_INTERNAL_captureLog', () => { it('includes SDK metadata in log attributes when available', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); const client = new TestClient(options); // Mock getSdkMetadata to return SDK info @@ -187,7 +187,7 @@ describe('_INTERNAL_captureLog', () => { it('does not include SDK metadata in log attributes when not available', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); const client = new TestClient(options); // Mock getSdkMetadata to return no SDK info @@ -205,7 +205,7 @@ describe('_INTERNAL_captureLog', () => { }); it('includes custom attributes in log', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); _INTERNAL_captureLog( @@ -232,7 +232,7 @@ describe('_INTERNAL_captureLog', () => { }); it('flushes logs buffer when it reaches max size', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); // Fill the buffer to max size (100 is the MAX_LOG_BUFFER_SIZE constant in client.ts) @@ -249,7 +249,7 @@ describe('_INTERNAL_captureLog', () => { }); it('does not flush logs buffer when it is empty', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); const mockSendEnvelope = vi.spyOn(client as any, 'sendEnvelope').mockImplementation(() => {}); _INTERNAL_flushLogsBuffer(client); @@ -257,7 +257,7 @@ describe('_INTERNAL_captureLog', () => { }); it('handles parameterized strings correctly', () => { - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); const parameterizedMessage = fmt`Hello ${'John'}, welcome to ${'Sentry'}`; @@ -290,7 +290,8 @@ describe('_INTERNAL_captureLog', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true, beforeSendLog }, + enableLogs: true, + beforeSendLog, }); const client = new TestClient(options); @@ -336,7 +337,8 @@ describe('_INTERNAL_captureLog', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true, beforeSendLog }, + enableLogs: true, + beforeSendLog, }); const client = new TestClient(options); @@ -360,7 +362,7 @@ describe('_INTERNAL_captureLog', () => { it('emits beforeCaptureLog and afterCaptureLog events', () => { const beforeCaptureLogSpy = vi.spyOn(TestClient.prototype, 'emit'); - const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } }); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); const client = new TestClient(options); const log: Log = { @@ -380,7 +382,7 @@ describe('_INTERNAL_captureLog', () => { it('includes user data in log attributes', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); const client = new TestClient(options); const scope = new Scope(); @@ -412,7 +414,7 @@ describe('_INTERNAL_captureLog', () => { it('includes partial user data when only some fields are available', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, }); const client = new TestClient(options); @@ -436,7 +438,7 @@ describe('_INTERNAL_captureLog', () => { it('includes user email and username without id', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, }); const client = new TestClient(options); @@ -465,7 +467,7 @@ describe('_INTERNAL_captureLog', () => { it('does not include user data when user object is empty', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, }); const client = new TestClient(options); @@ -481,7 +483,7 @@ describe('_INTERNAL_captureLog', () => { it('combines user data with other log attributes', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, release: '1.0.0', environment: 'test', @@ -535,7 +537,7 @@ describe('_INTERNAL_captureLog', () => { it('handles user data with non-string values', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, }); const client = new TestClient(options); @@ -564,7 +566,7 @@ describe('_INTERNAL_captureLog', () => { it('preserves existing user attributes in log and does not override them', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, }); const client = new TestClient(options); @@ -607,7 +609,7 @@ describe('_INTERNAL_captureLog', () => { it('only adds scope user data for attributes that do not already exist', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, sendDefaultPii: true, }); const client = new TestClient(options); @@ -656,7 +658,7 @@ describe('_INTERNAL_captureLog', () => { it('overrides user-provided system attributes with SDK values', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, release: 'sdk-release-1.0.0', environment: 'sdk-environment', }); diff --git a/packages/core/test/lib/server-runtime-client.test.ts b/packages/core/test/lib/server-runtime-client.test.ts index d5b9dc33681d..708ac8716070 100644 --- a/packages/core/test/lib/server-runtime-client.test.ts +++ b/packages/core/test/lib/server-runtime-client.test.ts @@ -211,7 +211,7 @@ describe('ServerRuntimeClient', () => { it('flushes logs when weight exceeds 800KB', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); client = new ServerRuntimeClient(options); @@ -228,7 +228,7 @@ describe('ServerRuntimeClient', () => { it('accumulates log weight without flushing when under threshold', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); client = new ServerRuntimeClient(options); @@ -245,7 +245,7 @@ describe('ServerRuntimeClient', () => { it('flushes logs on flush event', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); client = new ServerRuntimeClient(options); @@ -265,7 +265,6 @@ describe('ServerRuntimeClient', () => { it('does not flush logs when logs are disabled', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: false }, }); client = new ServerRuntimeClient(options); @@ -282,7 +281,7 @@ describe('ServerRuntimeClient', () => { it('flushes logs when flush event is triggered', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, - _experiments: { enableLogs: true }, + enableLogs: true, }); client = new ServerRuntimeClient(options); diff --git a/packages/node-core/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts index a485a6c56431..a58a3ea31ad0 100644 --- a/packages/node-core/src/integrations/winston.ts +++ b/packages/node-core/src/integrations/winston.ts @@ -28,7 +28,7 @@ interface WinstonTransportOptions { } /** - * Creates a new Sentry Winston transport that fowards logs to Sentry. Requires `_experiments.enableLogs` to be enabled. + * Creates a new Sentry Winston transport that fowards logs to Sentry. Requires the `enableLogs` option to be enabled. * * Supports Winston 3.x.x. * diff --git a/packages/node-core/src/logs/exports.ts b/packages/node-core/src/logs/exports.ts index c18b69f6770a..665d4d78d9ad 100644 --- a/packages/node-core/src/logs/exports.ts +++ b/packages/node-core/src/logs/exports.ts @@ -1,7 +1,7 @@ import { type CaptureLogArgs, captureLog } from './capture'; /** - * @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `trace` level. Requires the `enableLogs` option to be enabled. * * You can either pass a message and attributes or a message template, params and attributes. * @@ -28,7 +28,7 @@ export function trace(...args: CaptureLogArgs): void { } /** - * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `debug` level. Requires the `enableLogs` option to be enabled. * * You can either pass a message and attributes or a message template, params and attributes. * @@ -55,7 +55,7 @@ export function debug(...args: CaptureLogArgs): void { } /** - * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `info` level. Requires the `enableLogs` option to be enabled. * * You can either pass a message and attributes or a message template, params and attributes. * @@ -82,7 +82,7 @@ export function info(...args: CaptureLogArgs): void { } /** - * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `warn` level. Requires the `enableLogs` option to be enabled. * * You can either pass a message and attributes or a message template, params and attributes. * @@ -110,7 +110,7 @@ export function warn(...args: CaptureLogArgs): void { } /** - * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `error` level. Requires the `enableLogs` option to be enabled. * * You can either pass a message and attributes or a message template, params and attributes. * @@ -138,7 +138,7 @@ export function error(...args: CaptureLogArgs): void { } /** - * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `fatal` level. Requires the `enableLogs` option to be enabled. * * You can either pass a message and attributes or a message template, params and attributes. * diff --git a/packages/node-core/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts index 50b0d4d92b6e..6cbe6304a946 100644 --- a/packages/node-core/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -45,7 +45,10 @@ export class NodeClient extends ServerRuntimeClient { super(clientOptions); - if (this.getOptions()._experiments?.enableLogs) { + const { enableLogs, _experiments } = this.getOptions(); + // eslint-disable-next-line deprecation/deprecation + const shouldEnableLogs = enableLogs ?? _experiments?.enableLogs; + if (shouldEnableLogs) { this._logOnExitFlushListener = () => { _INTERNAL_flushLogsBuffer(this); }; diff --git a/packages/node-core/test/sdk/client.test.ts b/packages/node-core/test/sdk/client.test.ts index f053b1ba7e0e..7f57d4772212 100644 --- a/packages/node-core/test/sdk/client.test.ts +++ b/packages/node-core/test/sdk/client.test.ts @@ -296,7 +296,7 @@ describe('NodeClient', () => { describe('log capture', () => { it('adds server name to log attributes', () => { - const options = getDefaultNodeClientOptions({ _experiments: { enableLogs: true } }); + const options = getDefaultNodeClientOptions({ enableLogs: true }); const client = new NodeClient(options); const log: Log = { level: 'info', message: 'test message', attributes: {} }; @@ -309,7 +309,7 @@ describe('NodeClient', () => { it('preserves existing log attributes', () => { const serverName = 'test-server'; - const options = getDefaultNodeClientOptions({ serverName, _experiments: { enableLogs: true } }); + const options = getDefaultNodeClientOptions({ serverName, enableLogs: true }); const client = new NodeClient(options); const log: Log = { level: 'info', message: 'test message', attributes: { 'existing.attr': 'value' } }; diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index f053b1ba7e0e..7f57d4772212 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -296,7 +296,7 @@ describe('NodeClient', () => { describe('log capture', () => { it('adds server name to log attributes', () => { - const options = getDefaultNodeClientOptions({ _experiments: { enableLogs: true } }); + const options = getDefaultNodeClientOptions({ enableLogs: true }); const client = new NodeClient(options); const log: Log = { level: 'info', message: 'test message', attributes: {} }; @@ -309,7 +309,7 @@ describe('NodeClient', () => { it('preserves existing log attributes', () => { const serverName = 'test-server'; - const options = getDefaultNodeClientOptions({ serverName, _experiments: { enableLogs: true } }); + const options = getDefaultNodeClientOptions({ serverName, enableLogs: true }); const client = new NodeClient(options); const log: Log = { level: 'info', message: 'test message', attributes: { 'existing.attr': 'value' } }; diff --git a/packages/pino-transport/README.md b/packages/pino-transport/README.md index 0bb6aeed81ed..fdc077aa2b01 100644 --- a/packages/pino-transport/README.md +++ b/packages/pino-transport/README.md @@ -30,7 +30,7 @@ pnpm add @sentry/pino-transport pino - Node.js 18+ - Pino v8 or v9 -- `@sentry/node` SDK with `_experiments.enableLogs: true` +- `@sentry/node` SDK with `enableLogs: true` ## Setup @@ -41,9 +41,7 @@ import * as Sentry from '@sentry/node'; Sentry.init({ dsn: 'YOUR_DSN', - _experiments: { - enableLogs: true, - }, + enableLogs: true, }); ``` @@ -252,7 +250,7 @@ const logger = pino({ ### Logs not appearing in Sentry -1. Ensure `_experiments.enableLogs: true` is set in your Sentry configuration. +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. diff --git a/packages/pino-transport/src/index.ts b/packages/pino-transport/src/index.ts index 7bff4dc35327..986c7e892fc2 100644 --- a/packages/pino-transport/src/index.ts +++ b/packages/pino-transport/src/index.ts @@ -84,7 +84,7 @@ interface PinoSourceConfig { } /** - * Creates a new Sentry Pino transport that forwards logs to Sentry. Requires `_experiments.enableLogs` to be enabled. + * Creates a new Sentry Pino transport that forwards logs to Sentry. Requires the `enableLogs` option to be enabled. * * Supports Pino v8 and v9. * diff --git a/packages/vercel-edge/src/logs/exports.ts b/packages/vercel-edge/src/logs/exports.ts index ef2614b81f55..c21477e378b3 100644 --- a/packages/vercel-edge/src/logs/exports.ts +++ b/packages/vercel-edge/src/logs/exports.ts @@ -19,7 +19,7 @@ function captureLog( } /** - * @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `trace` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { userId: 100, route: '/dashboard' }. @@ -48,7 +48,7 @@ export function trace(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `debug` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { component: 'Header', state: 'loading' }. @@ -78,7 +78,7 @@ export function debug(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `info` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { feature: 'checkout', status: 'completed' }. @@ -108,7 +108,7 @@ export function info(message: ParameterizedString, attributes?: Log['attributes' } /** - * @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `warn` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { browser: 'Chrome', version: '91.0' }. @@ -139,7 +139,7 @@ export function warn(message: ParameterizedString, attributes?: Log['attributes' } /** - * @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `error` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { error: 'NetworkError', url: '/api/data' }. @@ -171,7 +171,7 @@ export function error(message: ParameterizedString, attributes?: Log['attributes } /** - * @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled. + * @summary Capture a log with the `fatal` level. Requires the `enableLogs` option to be enabled. * * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., { appState: 'corrupted', sessionId: 'abc-123' }. From 2f069f56382bd9d86dc9a16495160ca34d4e3264 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 21 Jul 2025 12:01:49 +0200 Subject: [PATCH 02/14] ref(v9/browser): Remove unnecessary CLS web vital check (#17096) Backport of #17090: Just a minor cleanup from a TODO I left a while ago. We can safely remove this check because CLS isn't reported before FCP is reported. --- packages/browser-utils/src/metrics/browserMetrics.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index e9fa822a431e..d182c64030fa 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -430,10 +430,8 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries delete _measurements['mark.fid']; } - // If FCP is not recorded we should not record the cls value - // according to the new definition of CLS. - // TODO: Check if the first condition is still necessary: `onCLS` already only fires once `onFCP` was called. - if (!('fcp' in _measurements) || !options.recordClsOnPageloadSpan) { + // If CLS standalone spans are enabled, don't record CLS as a measurement + if (!options.recordClsOnPageloadSpan) { delete _measurements.cls; } From 032955529366955eff7eeee07742cb9b8896c9bb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 21 Jul 2025 12:52:52 +0200 Subject: [PATCH 03/14] fix(v9/remix): Ensure source maps upload fails silently if Sentry CLI fails (#17095) Backport of #17082 --------- Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- packages/remix/package.json | 2 +- packages/remix/scripts/createRelease.js | 21 +++-- .../test/scripts/upload-sourcemaps.test.ts | 30 +++++++ yarn.lock | 88 +++++++++---------- 4 files changed, 90 insertions(+), 51 deletions(-) diff --git a/packages/remix/package.json b/packages/remix/package.json index ba0dccdb3786..47a6ed80f568 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -68,7 +68,7 @@ "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", - "@sentry/cli": "^2.46.0", + "@sentry/cli": "^2.50.0", "@sentry/core": "9.40.0", "@sentry/node": "9.40.0", "@sentry/react": "9.40.0", diff --git a/packages/remix/scripts/createRelease.js b/packages/remix/scripts/createRelease.js index ab969c48be18..d00005d89520 100644 --- a/packages/remix/scripts/createRelease.js +++ b/packages/remix/scripts/createRelease.js @@ -27,13 +27,22 @@ async function createRelease(argv, URL_PREFIX, BUILD_PATH) { await sentry.releases.new(release); - await sentry.releases.uploadSourceMaps(release, { - urlPrefix: URL_PREFIX, - include: [BUILD_PATH], - useArtifactBundle: !argv.disableDebugIds, - }); + try { + await sentry.releases.uploadSourceMaps(release, { + urlPrefix: URL_PREFIX, + include: [BUILD_PATH], + useArtifactBundle: !argv.disableDebugIds, + live: 'rejectOnError', + }); + } catch { + console.warn('[sentry] Failed to upload sourcemaps.'); + } - await sentry.releases.finalize(release); + try { + await sentry.releases.finalize(release); + } catch { + console.warn('[sentry] Failed to finalize release.'); + } if (argv.deleteAfterUpload) { try { diff --git a/packages/remix/test/scripts/upload-sourcemaps.test.ts b/packages/remix/test/scripts/upload-sourcemaps.test.ts index 0634930bf00e..677c602a011f 100644 --- a/packages/remix/test/scripts/upload-sourcemaps.test.ts +++ b/packages/remix/test/scripts/upload-sourcemaps.test.ts @@ -5,6 +5,8 @@ const uploadSourceMapsMock = vi.fn(); const finalizeMock = vi.fn(); const proposeVersionMock = vi.fn(() => '0.1.2.3.4'); +const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + // The createRelease script requires the Sentry CLI, which we need to mock so we // hook require to do this async function mock(mockedUri: string, stub: any) { @@ -56,6 +58,7 @@ describe('createRelease', () => { urlPrefix: '~/build/', include: ['public/build'], useArtifactBundle: true, + live: 'rejectOnError', }); expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3'); }); @@ -69,6 +72,7 @@ describe('createRelease', () => { urlPrefix: '~/build/', include: ['public/build'], useArtifactBundle: true, + live: 'rejectOnError', }); expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3.4'); }); @@ -89,9 +93,35 @@ describe('createRelease', () => { urlPrefix: '~/build/', include: ['public/build'], useArtifactBundle: true, + live: 'rejectOnError', }); expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3.4'); }); + + it('logs an error when uploadSourceMaps fails', async () => { + uploadSourceMapsMock.mockRejectedValue(new Error('Failed to upload sourcemaps')); + + await createRelease({}, '~/build/', 'public/build'); + + expect(uploadSourceMapsMock).toHaveBeenCalledWith('0.1.2.3.4', { + urlPrefix: '~/build/', + include: ['public/build'], + useArtifactBundle: true, + live: 'rejectOnError', + }); + + expect(consoleWarnSpy).toHaveBeenCalledWith('[sentry] Failed to upload sourcemaps.'); + + expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3.4'); + }); + + it('logs an error when finalize fails', async () => { + finalizeMock.mockRejectedValue(new Error('Failed to finalize release')); + + await createRelease({}, '~/build/', 'public/build'); + + expect(consoleWarnSpy).toHaveBeenCalledWith('[sentry] Failed to finalize release.'); + }); }); // To avoid `--isolatedModules` flag as we're not importing diff --git a/yarn.lock b/yarn.lock index 16bf8fe6d899..08c3ba2afb00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6981,75 +6981,75 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz#a32a4f226e717122b37d9969e8d4d0e14779f720" integrity sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg== -"@sentry/cli-darwin@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.46.0.tgz#e07ff66f03e8cb6e1988b7673ae5dbd6ff957b1d" - integrity sha512-5Ll+e5KAdIk9OYiZO8aifMBRNWmNyPjSqdjaHlBC1Qfh7pE3b1zyzoHlsUazG0bv0sNrSGea8e7kF5wIO1hvyg== +"@sentry/cli-darwin@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.50.0.tgz#0fec0ece84afe37b33464ccd514367fc95d507f3" + integrity sha512-Aj+cLBZ0dCw+pdUxvJ1U71PnKh2YjvpzLN9h1ZTe8UI3FqmkKkSH/J8mN/5qmR7qUHjDcm2l+wfgVUaaP8CWbA== "@sentry/cli-linux-arm64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz#1c06c83ff21f51ec23acf5be3b1f8c7553bf86b1" integrity sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw== -"@sentry/cli-linux-arm64@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.46.0.tgz#d5b27e5813e7b3add65c9e3dbdd75a8bea4ef324" - integrity sha512-OEJN8yAjI9y5B4telyqzu27Hi3+S4T8VxZCqJz1+z2Mp0Q/MZ622AahVPpcrVq/5bxrnlZR16+lKh8L1QwNFPg== +"@sentry/cli-linux-arm64@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.50.0.tgz#bbafacf82766d45ff05434cd7cabbda7005d1efd" + integrity sha512-p6hIh4Bb87qBfEz9w5dxEPAohIKcw68qoy5VUTx+cCanO8uXNWWsT78xtUNFRscW9zc6MxQMSITTWaCEIKvxRA== "@sentry/cli-linux-arm@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz#00cadc359ae3c051efb3e63873c033c61dbd1ca1" integrity sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg== -"@sentry/cli-linux-arm@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.46.0.tgz#d2a0f21cd208ef8e844bc5e565b337640d125441" - integrity sha512-WRrLNq/TEX/TNJkGqq6Ad0tGyapd5dwlxtsPbVBrIdryuL1mA7VCBoaHBr3kcwJLsgBHFH0lmkMee2ubNZZdkg== +"@sentry/cli-linux-arm@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.50.0.tgz#e1fed09b94c508db9de5353d8305828b0a3551e9" + integrity sha512-SGPAFwOY2of2C+RUBJcxMN2JXikVFEk8ypYOsQTEvV/48cLejcO/O2mHIj/YKgIkrfn3t7LlqdK6g75lkz+F8Q== "@sentry/cli-linux-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz#3b817b715dd806c20dfbffd539725ad8089c310a" integrity sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ== -"@sentry/cli-linux-i686@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.46.0.tgz#73368ebe30236c8647caec420f717a7f45410f29" - integrity sha512-xko3/BVa4LX8EmRxVOCipV+PwfcK5Xs8lP6lgF+7NeuAHMNL4DqF6iV9rrN8gkGUHCUI9RXSve37uuZnFy55+Q== +"@sentry/cli-linux-i686@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.50.0.tgz#95f0eb65bdde4c33e492830ae4ac207b60494f8e" + integrity sha512-umhGmbiCUG7MvjTm8lXFmFxQjyTVtYakilBwPTVzRELmNKxxhfKRxwSSA+hUKetAUzNd8fJx8K7yqdw+qRA7Pg== "@sentry/cli-linux-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz#ddf906bc3071cc79ce6e633eddcb76bb9068e688" integrity sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw== -"@sentry/cli-linux-x64@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.46.0.tgz#49da3dfd873e0e72abef968e1c213b9397e5d70e" - integrity sha512-hJ1g5UEboYcOuRia96LxjJ0jhnmk8EWLDvlGnXLnYHkwy3ree/L7sNgdp/QsY8Z4j2PGO5f22Va+UDhSjhzlfQ== +"@sentry/cli-linux-x64@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.50.0.tgz#5266b6b8660e6b72688331b7c702e9d1ca6413ed" + integrity sha512-ugIIx9+wUmguxOUe9ZVacvdCffZwqtFSKwpJ06Nqes0XfL4ZER4Qlq3/miCZ8m150C4xK5ym/QCwB41ffBqI4g== -"@sentry/cli-win32-arm64@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.46.0.tgz#4e26b254d5283eb114ac916ac504283a30b2ecdb" - integrity sha512-mN7cpPoCv2VExFRGHt+IoK11yx4pM4ADZQGEso5BAUZ5duViXB2WrAXCLd8DrwMnP0OE978a7N8OtzsFqjkbNA== +"@sentry/cli-win32-arm64@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.50.0.tgz#663d75fea42b853940c6faacf7ee76a16b449654" + integrity sha512-fMyBSKLrVHY9944t8oTpul+6osyQeuN8GGGP3diDxGQpynYL+vhcHZIpXFRH398+3kedG/IFoY7EwGgIEqWzmw== "@sentry/cli-win32-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz#9036085c7c6ce455ad45fda411c55ff39c06eb95" integrity sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw== -"@sentry/cli-win32-i686@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.46.0.tgz#72f7c0a611f17b7e5b34e2b47309d165195a8276" - integrity sha512-6F73AUE3lm71BISUO19OmlnkFD5WVe4/wA1YivtLZTc1RU3eUYJLYxhDfaH3P77+ycDppQ2yCgemLRaA4A8mNQ== +"@sentry/cli-win32-i686@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.50.0.tgz#96813ca970f35a839d7f817534ac556bc1df1567" + integrity sha512-VbC+l2Y2kB7Lsun2c8t7ZGwmljmXnyncZLW9PjdEyJSTAJ9GnEnSvyFSPXNLV/eHJnfQffzU7QTjU8vkQ7XMYg== "@sentry/cli-win32-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz#7d6464b63f32c9f97fff428f246b1f039b402233" integrity sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw== -"@sentry/cli-win32-x64@2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.46.0.tgz#8cfd438ec365b0ee925d9724a24b533b4cb75587" - integrity sha512-yuGVcfepnNL84LGA0GjHzdMIcOzMe0bjPhq/rwPsPN+zu11N+nPR2wV2Bum4U0eQdqYH3iAlMdL5/BEQfuLJww== +"@sentry/cli-win32-x64@2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.50.0.tgz#9f644efed8cb75943078a0ca4e414fa21dda6280" + integrity sha512-nMktyF93NtQUOViAAKHpHSWACOGjOkKjiewi4pD6W3sWllFiPPyt15XoyApqWwnICDRQu2DI5vnil4ck6/k7mw== "@sentry/cli@2.42.2": version "2.42.2" @@ -7070,10 +7070,10 @@ "@sentry/cli-win32-i686" "2.42.2" "@sentry/cli-win32-x64" "2.42.2" -"@sentry/cli@^2.36.1", "@sentry/cli@^2.46.0": - version "2.46.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.46.0.tgz#790864874ea04f804053aa85dc94501b2cc321bb" - integrity sha512-nqoPl7UCr446QFkylrsRrUXF51x8Z9dGquyf4jaQU+OzbOJMqclnYEvU6iwbwvaw3tu/2DnoZE/Og+Nq1h63sA== +"@sentry/cli@^2.36.1", "@sentry/cli@^2.46.0", "@sentry/cli@^2.50.0": + version "2.50.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.50.0.tgz#7e2298bea9a2bb50126bfb24116ae98199bc1f6f" + integrity sha512-OHRRQPUNjBpzOT6arNhxXQ71DKs5jSziCfDzmEGwAs+K8J/I1QxnvJkto88HbXE54oiWhSEJwL0pvcowFXyVbA== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -7081,14 +7081,14 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.46.0" - "@sentry/cli-linux-arm" "2.46.0" - "@sentry/cli-linux-arm64" "2.46.0" - "@sentry/cli-linux-i686" "2.46.0" - "@sentry/cli-linux-x64" "2.46.0" - "@sentry/cli-win32-arm64" "2.46.0" - "@sentry/cli-win32-i686" "2.46.0" - "@sentry/cli-win32-x64" "2.46.0" + "@sentry/cli-darwin" "2.50.0" + "@sentry/cli-linux-arm" "2.50.0" + "@sentry/cli-linux-arm64" "2.50.0" + "@sentry/cli-linux-i686" "2.50.0" + "@sentry/cli-linux-x64" "2.50.0" + "@sentry/cli-win32-arm64" "2.50.0" + "@sentry/cli-win32-i686" "2.50.0" + "@sentry/cli-win32-x64" "2.50.0" "@sentry/rollup-plugin@^3.5.0": version "3.5.0" From a5259dce6ee8c67908f37f39cc5a7b8e52b3e3fb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 24 Jul 2025 09:53:21 +0200 Subject: [PATCH 04/14] fix(v9/svelte): Do not insert preprocess code in script module in Svelte 5 (#17124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/getsentry/sentry-javascript/pull/17114 --------- Co-authored-by: Richard Jelínek <131378741+richardjelinek-fastest@users.noreply.github.com> --- CHANGELOG.md | 2 ++ packages/svelte/src/preprocessors.ts | 2 +- packages/svelte/test/preprocessors.test.ts | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89bc7a377ee5..53ea2ac0304c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @richardjelinek-fastest. Thank you for your contribution! + ## 9.40.0 ### Important Changes diff --git a/packages/svelte/src/preprocessors.ts b/packages/svelte/src/preprocessors.ts index b13b20ec59f6..c7df0e258c0f 100644 --- a/packages/svelte/src/preprocessors.ts +++ b/packages/svelte/src/preprocessors.ts @@ -97,7 +97,7 @@ function shouldInjectFunction( // because the code inside is not executed when the component is instantiated but // when the module is first imported. // see: https://svelte.dev/docs#component-format-script-context-module - if (attributes.context === 'module') { + if (attributes.module || attributes.context === 'module') { return false; } diff --git a/packages/svelte/test/preprocessors.test.ts b/packages/svelte/test/preprocessors.test.ts index 547f58b366ab..207e2d95cce5 100644 --- a/packages/svelte/test/preprocessors.test.ts +++ b/packages/svelte/test/preprocessors.test.ts @@ -170,6 +170,26 @@ describe('componentTrackingPreprocessor', () => { expect(processedComponent.newCode).toEqual(processedComponent.originalCode); }); + + it('doesnt inject the function call to a module context script block with Svelte 5 module attribute', () => { + const preProc = componentTrackingPreprocessor(); + const component = { + originalCode: 'console.log(cmp2)', + filename: 'lib/Cmp2.svelte', + name: 'Cmp2', + }; + + const res: any = preProc.script?.({ + content: component.originalCode, + filename: component.filename, + attributes: { module: true }, + markup: '', + }); + + const processedComponent = { ...component, newCode: res.code, map: res.map }; + + expect(processedComponent.newCode).toEqual(processedComponent.originalCode); + }); }); describe('markup hook', () => { From b6f5f329d62221347705a30cbf5c293a9a470a16 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 24 Jul 2025 09:53:44 +0200 Subject: [PATCH 05/14] fix(v9/cloudflare) Allow non UUID workflow instance IDs (#17135) - A backport to v9 of #17121 - Closes #17074 @StephenHaney unless you're a very early adopter of v10, this PR is probably the one you care about! --- packages/cloudflare/src/workflows.ts | 26 +++++--- packages/cloudflare/test/workflow.test.ts | 78 ++++++++++++++++++++++- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/cloudflare/src/workflows.ts b/packages/cloudflare/src/workflows.ts index 022f0040893a..7ca31d00bbd5 100644 --- a/packages/cloudflare/src/workflows.ts +++ b/packages/cloudflare/src/workflows.ts @@ -24,14 +24,24 @@ import { init } from './sdk'; const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i; -function propagationContextFromInstanceId(instanceId: string): PropagationContext { - // Validate and normalize traceId - should be a valid UUID with or without hyphens - if (!UUID_REGEX.test(instanceId)) { - throw new Error("Invalid 'instanceId' for workflow: Sentry requires random UUIDs for instanceId."); - } +/** + * Hashes a string to a UUID using SHA-1. + */ +export async function deterministicTraceIdFromInstanceId(instanceId: string): Promise { + const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(instanceId)); + return ( + Array.from(new Uint8Array(buf)) + // We only need the first 16 bytes for the 32 characters + .slice(0, 16) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + ); +} - // Remove hyphens to get UUID without hyphens - const traceId = instanceId.replace(/-/g, ''); +async function propagationContextFromInstanceId(instanceId: string): Promise { + const traceId = UUID_REGEX.test(instanceId) + ? instanceId.replace(/-/g, '') + : await deterministicTraceIdFromInstanceId(instanceId); // Derive sampleRand from last 4 characters of the random UUID // @@ -60,7 +70,7 @@ async function workflowStepWithSentry( addCloudResourceContext(isolationScope); return withScope(async scope => { - const propagationContext = propagationContextFromInstanceId(instanceId); + const propagationContext = await propagationContextFromInstanceId(instanceId); scope.setPropagationContext(propagationContext); // eslint-disable-next-line no-return-await diff --git a/packages/cloudflare/test/workflow.test.ts b/packages/cloudflare/test/workflow.test.ts index 03eee5191eb2..c403023fb525 100644 --- a/packages/cloudflare/test/workflow.test.ts +++ b/packages/cloudflare/test/workflow.test.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { WorkflowEvent, WorkflowStep, WorkflowStepConfig } from 'cloudflare:workers'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { instrumentWorkflowWithSentry } from '../src/workflows'; +import { deterministicTraceIdFromInstanceId, instrumentWorkflowWithSentry } from '../src/workflows'; + +const NODE_MAJOR_VERSION = parseInt(process.versions.node.split('.')[0]!); const mockStep: WorkflowStep = { do: vi @@ -63,11 +65,18 @@ const INSTANCE_ID = 'ae0ee067-61b3-4852-9219-5d62282270f0'; const SAMPLE_RAND = '0.44116884107728693'; const TRACE_ID = INSTANCE_ID.replace(/-/g, ''); -describe('workflows', () => { +describe.skipIf(NODE_MAJOR_VERSION < 20)('workflows', () => { beforeEach(() => { vi.clearAllMocks(); }); + test('hashStringToUuid hashes a string to a UUID for Sentry trace ID', async () => { + const UUID_WITHOUT_HYPHENS_REGEX = /^[0-9a-f]{32}$/i; + expect(await deterministicTraceIdFromInstanceId('s')).toMatch(UUID_WITHOUT_HYPHENS_REGEX); + expect(await deterministicTraceIdFromInstanceId('test-string')).toMatch(UUID_WITHOUT_HYPHENS_REGEX); + expect(await deterministicTraceIdFromInstanceId(INSTANCE_ID)).toMatch(UUID_WITHOUT_HYPHENS_REGEX); + }); + test('Calls expected functions', async () => { class BasicTestWorkflow { constructor(_ctx: ExecutionContext, _env: unknown) {} @@ -133,6 +142,71 @@ describe('workflows', () => { ]); }); + test('Calls expected functions with non-uuid instance id', async () => { + class BasicTestWorkflow { + constructor(_ctx: ExecutionContext, _env: unknown) {} + + async run(_event: Readonly>, step: WorkflowStep): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const files = await step.do('first step', async () => { + return { files: ['doc_7392_rev3.pdf', 'report_x29_final.pdf'] }; + }); + } + } + + const TestWorkflowInstrumented = instrumentWorkflowWithSentry(getSentryOptions, BasicTestWorkflow as any); + const workflow = new TestWorkflowInstrumented(mockContext, {}) as BasicTestWorkflow; + const event = { payload: {}, timestamp: new Date(), instanceId: 'ae0ee067' }; + await workflow.run(event, mockStep); + + expect(mockStep.do).toHaveBeenCalledTimes(1); + expect(mockStep.do).toHaveBeenCalledWith('first step', expect.any(Function)); + expect(mockContext.waitUntil).toHaveBeenCalledTimes(1); + expect(mockContext.waitUntil).toHaveBeenCalledWith(expect.any(Promise)); + expect(mockTransport.send).toHaveBeenCalledTimes(1); + expect(mockTransport.send).toHaveBeenCalledWith([ + expect.objectContaining({ + trace: expect.objectContaining({ + transaction: 'first step', + trace_id: '0d2b6d1743ce6d53af4f5ee416ad5d1b', + sample_rand: '0.3636987869077592', + }), + }), + [ + [ + { + type: 'transaction', + }, + expect.objectContaining({ + event_id: expect.any(String), + contexts: { + trace: { + parent_span_id: undefined, + span_id: expect.any(String), + trace_id: '0d2b6d1743ce6d53af4f5ee416ad5d1b', + data: { + 'sentry.origin': 'auto.faas.cloudflare.workflow', + 'sentry.op': 'function.step.do', + 'sentry.source': 'task', + 'sentry.sample_rate': 1, + }, + op: 'function.step.do', + status: 'ok', + origin: 'auto.faas.cloudflare.workflow', + }, + cloud_resource: { 'cloud.provider': 'cloudflare' }, + runtime: { name: 'cloudflare' }, + }, + type: 'transaction', + transaction_info: { source: 'task' }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ], + ], + ]); + }); + class ErrorTestWorkflow { count = 0; constructor(_ctx: ExecutionContext, _env: unknown) {} From b6cad7b4e3a828a895525706115d2c6a8321b7e7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 24 Jul 2025 09:56:15 +0200 Subject: [PATCH 06/14] feat(v9/node): Add shouldHandleError option to fastifyIntegration (#17123) Backport of #16845 closes https://github.com/getsentry/sentry-javascript/pull/17123 --------- Co-authored-by: Onur Temizkan --- .../node-fastify-3/package.json | 4 +- .../playwright.override.config.mjs | 7 + .../src/app-handle-error-override.ts | 178 +++++++++++++++++ .../node-fastify-3/src/app.ts | 19 +- .../node-fastify-3/tests/errors.test.ts | 15 ++ .../node-fastify-4/package.json | 4 +- .../playwright.override.config.mjs | 7 + .../src/app-handle-error-override.ts | 187 ++++++++++++++++++ .../node-fastify-4/src/app.ts | 18 +- .../node-fastify-4/tests/errors.test.ts | 15 ++ .../node-fastify-5/package.json | 4 +- .../playwright.override.config.mjs | 7 + .../src/app-handle-error-override.ts | 178 +++++++++++++++++ .../node-fastify-5/src/app.ts | 18 +- .../node-fastify-5/tests/errors.test.ts | 15 ++ .../src/integrations/tracing/fastify/index.ts | 82 ++++++-- 16 files changed, 741 insertions(+), 17 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-3/playwright.override.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.override.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json index 3beb69e26e59..006a33585fd3 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/package.json @@ -4,11 +4,13 @@ "private": true, "scripts": { "start": "ts-node src/app.ts", + "start:override": "ts-node src/app-handle-error-override.ts", "test": "playwright test", + "test:override": "playwright test --config playwright.override.config.mjs", "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", - "test:assert": "pnpm test" + "test:assert": "pnpm test && pnpm test:override" }, "dependencies": { "@sentry/node": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/playwright.override.config.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-3/playwright.override.config.mjs new file mode 100644 index 000000000000..ee1f461c57c8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/playwright.override.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start:override`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts new file mode 100644 index 000000000000..378ef99fa309 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app-handle-error-override.ts @@ -0,0 +1,178 @@ +import type * as S from '@sentry/node'; +const Sentry = require('@sentry/node') as typeof S; + +// We wrap console.warn to find out if a warning is incorrectly logged +console.warn = new Proxy(console.warn, { + apply: function (target, thisArg, argumentsList) { + const msg = argumentsList[0]; + if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { + console.error(`Sentry warning was triggered: ${msg}`); + process.exit(1); + } + + return target.apply(thisArg, argumentsList); + }, +}); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + return true; + }, + }), + ], + tracesSampleRate: 1, + tunnel: 'http://localhost:3031/', // proxy server + tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], +}); + +import type * as H from 'http'; +import type * as F from 'fastify'; + +// Make sure fastify is imported after Sentry is initialized +const { fastify } = require('fastify') as typeof F; +const http = require('http') as typeof H; + +const app = fastify(); +const port = 3030; +const port2 = 3040; + +Sentry.setupFastifyErrorHandler(app, { + shouldHandleError: (error, _request, _reply) => { + // @ts-ignore // Fastify V3 is not typed correctly + if (_request.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, +}); + +app.get('/test-success', function (_req, res) { + res.send({ version: 'v1' }); +}); + +app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { + const headers = req.headers; + + res.send({ headers, id: req.params.id }); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { + const id = req.params.id; + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); + + res.send(data); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { + const id = req.params.id; + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); + const data = await response.json(); + + res.send(data); +}); + +app.get('/test-transaction', async function (req, res) { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + res.send({}); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + +app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-http-external-allowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); + res.send(data); +}); + +app.get('/test-outgoing-http-external-disallowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); + res.send(data); +}); + +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + +app.listen({ port: port }); + +// A second app so we can test header propagation between external URLs +const app2 = fastify(); +app2.get('/external-allowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-allowed' }); +}); + +app2.get('/external-disallowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-disallowed' }); +}); + +app2.listen({ port: port2 }); + +function makeHttpRequest(url: string) { + return new Promise(resolve => { + const data: any[] = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); + httpRes.on('end', () => { + try { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + } catch { + resolve({ data: Buffer.concat(data).toString(), url }); + } + }); + }) + .end(); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts index 73ffafcfd04d..5b4a2f0d16ac 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/src/app.ts @@ -17,7 +17,19 @@ console.warn = new Proxy(console.warn, { Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, - integrations: [], + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + // @ts-ignore // Fastify V3 is not typed correctly + if (_request.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, + }), + ], tracesSampleRate: 1, tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], @@ -81,6 +93,11 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { throw new Error(`This is an exception with id ${req.params.id}`); }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts index 1a37fc244413..c0be1b0292a3 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/errors.test.ts @@ -28,3 +28,18 @@ test('Sends correct error event', async ({ baseURL }) => { parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); + +test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-3', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured'; + }); + + errorEventPromise.then(() => { + throw new Error('This error should not be captured'); + }); + + await fetch(`${baseURL}/test-error-not-captured`); + + // wait for a short time to ensure the error is not captured + await new Promise(resolve => setTimeout(resolve, 1000)); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json index 7441e4335fb4..01f4879d5a68 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/package.json @@ -4,11 +4,13 @@ "private": true, "scripts": { "start": "ts-node src/app.ts", + "start:override": "ts-node src/app-handle-error-override.ts", "test": "playwright test", + "test:override": "playwright test --config playwright.override.config.mjs", "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", - "test:assert": "pnpm test" + "test:assert": "pnpm test && pnpm test:override" }, "dependencies": { "@sentry/node": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.override.config.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.override.config.mjs new file mode 100644 index 000000000000..ee1f461c57c8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/playwright.override.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start:override`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts new file mode 100644 index 000000000000..72270efc05de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app-handle-error-override.ts @@ -0,0 +1,187 @@ +import type * as S from '@sentry/node'; +const Sentry = require('@sentry/node') as typeof S; + +// We wrap console.warn to find out if a warning is incorrectly logged +console.warn = new Proxy(console.warn, { + apply: function (target, thisArg, argumentsList) { + const msg = argumentsList[0]; + if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { + console.error(`Sentry warning was triggered: ${msg}`); + process.exit(1); + } + + return target.apply(thisArg, argumentsList); + }, +}); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + return true; + }, + }), + ], + tracesSampleRate: 1, + tunnel: 'http://localhost:3031/', // proxy server + tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], +}); + +import type * as H from 'http'; +import type * as F from 'fastify'; + +// Make sure fastify is imported after Sentry is initialized +const { fastify } = require('fastify') as typeof F; +const http = require('http') as typeof H; + +const app = fastify(); +const port = 3030; +const port2 = 3040; + +Sentry.setupFastifyErrorHandler(app, { + shouldHandleError: (error, _request, _reply) => { + if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, +}); + +app.get('/test-success', function (_req, res) { + res.send({ version: 'v1' }); +}); + +app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { + const headers = req.headers; + + res.send({ headers, id: req.params.id }); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { + const id = req.params.id; + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); + + res.send(data); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { + const id = req.params.id; + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); + const data = await response.json(); + + res.send(data); +}); + +app.get('/test-transaction', async function (req, res) { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + res.send({}); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + +app.get('/test-4xx-error', async function (req, res) { + res.code(400); + throw new Error('This is a 4xx error'); +}); + +app.get('/test-5xx-error', async function (req, res) { + res.code(500); + throw new Error('This is a 5xx error'); +}); + +app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-http-external-allowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); + res.send(data); +}); + +app.get('/test-outgoing-http-external-disallowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); + res.send(data); +}); + +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + +app.listen({ port: port }); + +// A second app so we can test header propagation between external URLs +const app2 = fastify(); +app2.get('/external-allowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-allowed' }); +}); + +app2.get('/external-disallowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-disallowed' }); +}); + +app2.listen({ port: port2 }); + +function makeHttpRequest(url: string) { + return new Promise(resolve => { + const data: any[] = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); + httpRes.on('end', () => { + try { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + } catch { + resolve({ data: Buffer.concat(data).toString(), url }); + } + }); + }) + .end(); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts index 7f7ac390b4b3..1c428c0486f9 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/src/app.ts @@ -17,7 +17,18 @@ console.warn = new Proxy(console.warn, { Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, - integrations: [], + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, + }), + ], tracesSampleRate: 1, tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], @@ -81,6 +92,11 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + app.get('/test-4xx-error', async function (req, res) { res.code(400); throw new Error('This is a 4xx error'); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts index 8ecdc8975778..46453e4749e0 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/errors.test.ts @@ -50,3 +50,18 @@ test('Does not send 4xx errors by default', async ({ baseURL }) => { const errorEvent = await serverErrorPromise; expect(errorEvent.exception?.values?.[0]?.value).toContain('This is a 5xx error'); }); + +test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-4', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured'; + }); + + errorEventPromise.then(() => { + throw new Error('This error should not be captured'); + }); + + await fetch(`${baseURL}/test-error-not-captured`); + + // wait for a short time to ensure the error is not captured + await new Promise(resolve => setTimeout(resolve, 1000)); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json index fba10d4d2a90..0b03a26eca47 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/package.json @@ -4,11 +4,13 @@ "private": true, "scripts": { "start": "ts-node src/app.ts", + "start:override": "ts-node src/app-handle-error-override.ts", "test": "playwright test", + "test:override": "playwright test --config playwright.override.config.mjs", "clean": "npx rimraf node_modules pnpm-lock.yaml", "typecheck": "tsc", "test:build": "pnpm install && pnpm run typecheck", - "test:assert": "pnpm test" + "test:assert": "pnpm test && pnpm test:override" }, "dependencies": { "@sentry/node": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs b/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/playwright.override.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts new file mode 100644 index 000000000000..217201332e17 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app-handle-error-override.ts @@ -0,0 +1,178 @@ +import type * as S from '@sentry/node'; +const Sentry = require('@sentry/node') as typeof S; + +// We wrap console.warn to find out if a warning is incorrectly logged +console.warn = new Proxy(console.warn, { + apply: function (target, thisArg, argumentsList) { + const msg = argumentsList[0]; + if (typeof msg === 'string' && msg.startsWith('[Sentry]')) { + console.error(`Sentry warning was triggered: ${msg}`); + process.exit(1); + } + + return target.apply(thisArg, argumentsList); + }, +}); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + return true; + }, + }), + ], + tracesSampleRate: 1, + tunnel: 'http://localhost:3031/', // proxy server + tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], +}); + +import type * as H from 'http'; +import type * as F from 'fastify'; + +// Make sure fastify is imported after Sentry is initialized +const { fastify } = require('fastify') as typeof F; +const http = require('http') as typeof H; + +const app = fastify(); +const port = 3030; +const port2 = 3040; + +Sentry.setupFastifyErrorHandler(app, { + shouldHandleError: (error, _request, _reply) => { + // @ts-ignore // Fastify V5 is not typed correctly + if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, +}); + +app.get('/test-success', function (_req, res) { + res.send({ version: 'v1' }); +}); + +app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) { + const headers = req.headers; + + res.send({ headers, id: req.params.id }); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) { + const id = req.params.id; + const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`); + + res.send(data); +}); + +app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) { + const id = req.params.id; + const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`); + const data = await response.json(); + + res.send(data); +}); + +app.get('/test-transaction', async function (req, res) { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + res.send({}); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + +app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +app.get('/test-outgoing-fetch-external-allowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) { + const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`); + const data = await fetchResponse.json(); + + res.send(data); +}); + +app.get('/test-outgoing-http-external-allowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`); + res.send(data); +}); + +app.get('/test-outgoing-http-external-disallowed', async function (req, res) { + const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`); + res.send(data); +}); + +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + +app.listen({ port: port }); + +// A second app so we can test header propagation between external URLs +const app2 = fastify(); +app2.get('/external-allowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-allowed' }); +}); + +app2.get('/external-disallowed', function (req, res) { + const headers = req.headers; + + res.send({ headers, route: '/external-disallowed' }); +}); + +app2.listen({ port: port2 }); + +function makeHttpRequest(url: string) { + return new Promise(resolve => { + const data: any[] = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('error', error => { + resolve({ error: error.message, url }); + }); + httpRes.on('end', () => { + try { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + } catch { + resolve({ data: Buffer.concat(data).toString(), url }); + } + }); + }) + .end(); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts index db2e9bf9cc5f..83f7e53a45ce 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/src/app.ts @@ -17,7 +17,18 @@ console.warn = new Proxy(console.warn, { Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.E2E_TEST_DSN, - integrations: [], + integrations: [ + Sentry.fastifyIntegration({ + shouldHandleError: (error, _request, _reply) => { + if (_request.routeOptions?.url?.includes('/test-error-not-captured')) { + // Errors from this path will not be captured by Sentry + return false; + } + + return true; + }, + }), + ], tracesSampleRate: 1, tunnel: 'http://localhost:3031/', // proxy server tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], @@ -79,6 +90,11 @@ app.get('/test-error', async function (req, res) { res.send({ exceptionId }); }); +app.get('/test-error-not-captured', async function () { + // This error will not be captured by Sentry + throw new Error('This is an error that will not be captured'); +}); + app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) { throw new Error(`This is an exception with id ${req.params.id}`); }); diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts index f79eb30e9b4c..cf1428b7a34a 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/errors.test.ts @@ -28,3 +28,18 @@ test('Sends correct error event', async ({ baseURL }) => { parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), }); }); + +test('Does not send error when shouldHandleError returns false', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-fastify-5', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an error that will not be captured'; + }); + + errorEventPromise.then(() => { + throw new Error('This error should not be captured'); + }); + + await fetch(`${baseURL}/test-error-not-captured`); + + // wait for a short time to ensure the error is not captured + await new Promise(resolve => setTimeout(resolve, 1000)); +}); diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index 0aaf7814e9e3..bb8333e73cf0 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -17,6 +17,41 @@ import { FastifyOtelInstrumentation } from './fastify-otel/index'; import type { FastifyInstance, FastifyReply, FastifyRequest } from './types'; import { FastifyInstrumentationV3 } from './v3/instrumentation'; +/** + * Options for the Fastify integration. + * + * `shouldHandleError` - Callback method deciding whether error should be captured and sent to Sentry + * This is used on Fastify v5 where Sentry handles errors in the diagnostics channel. + * Fastify v3 and v4 use `setupFastifyErrorHandler` instead. + * + * @example + * + * ```javascript + * Sentry.init({ + * integrations: [ + * Sentry.fastifyIntegration({ + * shouldHandleError(_error, _request, reply) { + * return reply.statusCode >= 500; + * }, + * }); + * }, + * }); + * ``` + * + */ +interface FastifyIntegrationOptions { + /** + * Callback method deciding whether error should be captured and sent to Sentry + * This is used on Fastify v5 where Sentry handles errors in the diagnostics channel. + * Fastify v3 and v4 use `setupFastifyErrorHandler` instead. + * + * @param error Captured Fastify error + * @param request Fastify request (or any object containing at least method, routeOptions.url, and routerPath) + * @param reply Fastify reply (or any object containing at least statusCode) + */ + shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; +} + interface FastifyHandlerOptions { /** * Callback method deciding whether error should be captured and sent to Sentry @@ -27,6 +62,7 @@ interface FastifyHandlerOptions { * * @example * + * * ```javascript * setupFastifyErrorHandler(app, { * shouldHandleError(_error, _request, reply) { @@ -35,6 +71,7 @@ interface FastifyHandlerOptions { * }); * ``` * + * * If using TypeScript, you can cast the request and reply to get full type safety. * * ```typescript @@ -53,10 +90,20 @@ interface FastifyHandlerOptions { } const INTEGRATION_NAME = 'Fastify'; +const INTEGRATION_NAME_V5 = 'Fastify-V5'; const INTEGRATION_NAME_V3 = 'Fastify-V3'; export const instrumentFastifyV3 = generateInstrumentOnce(INTEGRATION_NAME_V3, () => new FastifyInstrumentationV3()); +function getFastifyIntegration(): ReturnType | undefined { + const client = getClient(); + if (!client) { + return undefined; + } else { + return client.getIntegrationByName(INTEGRATION_NAME) as ReturnType | undefined; + } +} + function handleFastifyError( this: { diagnosticsChannelExists?: boolean; @@ -64,9 +111,9 @@ function handleFastifyError( error: Error, request: FastifyRequest & { opentelemetry?: () => { span?: Span } }, reply: FastifyReply, - shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean, handlerOrigin: 'diagnostics-channel' | 'onError-hook', ): void { + const shouldHandleError = getFastifyIntegration()?.getShouldHandleError() || defaultShouldHandleError; // Diagnostics channel runs before the onError hook, so we can use it to check if the handler was already registered if (handlerOrigin === 'diagnostics-channel') { this.diagnosticsChannelExists = true; @@ -76,7 +123,7 @@ function handleFastifyError( DEBUG_BUILD && debug.warn( 'Fastify error handler was already registered via diagnostics channel.', - 'You can safely remove `setupFastifyErrorHandler` call.', + 'You can safely remove `setupFastifyErrorHandler` call and set `shouldHandleError` on the integration options.', ); // If the diagnostics channel already exists, we don't need to handle the error again @@ -88,11 +135,9 @@ function handleFastifyError( } } -export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => { +export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME_V5, () => { const fastifyOtelInstrumentationInstance = new FastifyOtelInstrumentation(); const plugin = fastifyOtelInstrumentationInstance.plugin(); - const options = fastifyOtelInstrumentationInstance.getConfig(); - const shouldHandleError = (options as FastifyHandlerOptions)?.shouldHandleError || defaultShouldHandleError; // This message handler works for Fastify versions 3, 4 and 5 diagnosticsChannel.subscribe('fastify.initialization', message => { @@ -120,20 +165,30 @@ export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => reply: FastifyReply; }; - handleFastifyError.call(handleFastifyError, error, request, reply, shouldHandleError, 'diagnostics-channel'); + handleFastifyError.call(handleFastifyError, error, request, reply, 'diagnostics-channel'); }); // Returning this as unknown not to deal with the internal types of the FastifyOtelInstrumentation - return fastifyOtelInstrumentationInstance as Instrumentation; + return fastifyOtelInstrumentationInstance as Instrumentation; }); -const _fastifyIntegration = (() => { +const _fastifyIntegration = (({ shouldHandleError }: Partial) => { + let _shouldHandleError: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean; + return { name: INTEGRATION_NAME, setupOnce() { + _shouldHandleError = shouldHandleError || defaultShouldHandleError; + instrumentFastifyV3(); instrumentFastify(); }, + getShouldHandleError() { + return _shouldHandleError; + }, + setShouldHandleError(fn: (error: Error, request: FastifyRequest, reply: FastifyReply) => boolean): void { + _shouldHandleError = fn; + }, }; }) satisfies IntegrationFn; @@ -153,7 +208,9 @@ const _fastifyIntegration = (() => { * }) * ``` */ -export const fastifyIntegration = defineIntegration(_fastifyIntegration); +export const fastifyIntegration = defineIntegration((options: Partial = {}) => + _fastifyIntegration(options), +); /** * Default function to determine if an error should be sent to Sentry @@ -187,11 +244,14 @@ function defaultShouldHandleError(_error: Error, _request: FastifyRequest, reply * ``` */ export function setupFastifyErrorHandler(fastify: FastifyInstance, options?: Partial): void { - const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; + if (options?.shouldHandleError) { + getFastifyIntegration()?.setShouldHandleError(options.shouldHandleError); + } + const plugin = Object.assign( function (fastify: FastifyInstance, _options: unknown, done: () => void): void { fastify.addHook('onError', async (request, reply, error) => { - handleFastifyError.call(handleFastifyError, error, request, reply, shouldHandleError, 'onError-hook'); + handleFastifyError.call(handleFastifyError, error, request, reply, 'onError-hook'); }); done(); }, From dac0aff5319006d7405b42f6054120c800783513 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:33:34 +0200 Subject: [PATCH 07/14] feat(v9/astro): Parametrize dynamic server routes (#17141) Backport of all server-related parametrization in Astro: - https://github.com/getsentry/sentry-javascript/pull/17054 - https://github.com/getsentry/sentry-javascript/pull/17085 - https://github.com/getsentry/sentry-javascript/pull/17102 - https://github.com/getsentry/sentry-javascript/pull/17105 --- .../src/pages/api/user/[userId].json.js | 10 + .../src/pages/catchAll/[...path].astro | 12 ++ .../src/pages/user-page/[userId].astro | 17 ++ .../astro-4/tests/tracing.dynamic.test.ts | 193 ++++++++++++++++++ .../src/pages/catchAll/[...path].astro | 12 ++ .../src/pages/user-page/settings.astro | 7 + .../astro-5/tests/tracing.dynamic.test.ts | 107 +++++++++- .../tests/tracing.serverIslands.test.ts | 2 +- .../astro-5/tests/tracing.static.test.ts | 2 +- packages/astro/src/integration/index.ts | 57 +++++- packages/astro/src/integration/types.ts | 25 +++ packages/astro/src/server/middleware.ts | 19 +- 12 files changed, 451 insertions(+), 12 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js create mode 100644 dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro create mode 100644 dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js new file mode 100644 index 000000000000..562bbf7b2cb4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/api/user/[userId].json.js @@ -0,0 +1,10 @@ +export const prerender = false; + +export function GET({ params }) { + return new Response( + JSON.stringify({ + greeting: `Hello ${params.userId}`, + userId: params.userId, + }), + ); +} diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro new file mode 100644 index 000000000000..bb225c166241 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/catchAll/[...path].astro @@ -0,0 +1,12 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const params = Astro.params; + +--- + + +

params: {params}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro new file mode 100644 index 000000000000..e35bd3a34d97 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/[userId].astro @@ -0,0 +1,17 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const { userId } = Astro.params; + +const response = await fetch(Astro.url.origin + `/api/user/${userId}.json`) +const data = await response.json(); + +--- + + +

{data.greeting}

+ +

data: {JSON.stringify(data)}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts index 644afc377545..2699e827718e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts @@ -120,3 +120,196 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { }); }); }); + +test.describe('nested SSR routes (client, server, server request)', () => { + /** The user-page route fetches from an endpoint and creates a deeply nested span structure: + * pageload — /user-page/myUsername123 + * ├── browser.** — multiple browser spans + * └── browser.request — /user-page/myUsername123 + * └── http.server — GET /user-page/[userId] (SSR page request) + * └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span) + * └── http.server — GET /api/user/myUsername123.json (server request) + */ + test('sends connected server and client pageload and request spans with the same trace id', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; + }); + + await page.goto('/user-page/myUsername123'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; + const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( + span => span.op === 'http.client' && span.description?.includes('/api/user/'), + ); + + const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + + // Verify all spans have the same trace ID + expect(clientPageloadTraceId).toEqual(serverPageRequestTxn.contexts?.trace?.trace_id); + expect(clientPageloadTraceId).toEqual(serverHTTPServerRequestTxn.contexts?.trace?.trace_id); + expect(clientPageloadTraceId).toEqual(serverRequestHTTPClientSpan?.trace_id); + + // serverPageRequest has no parent (root span) + expect(serverPageRequestTxn.contexts?.trace?.parent_span_id).toBeUndefined(); + + // clientPageload's parent and serverRequestHTTPClient's parent is serverPageRequest + const serverPageRequestSpanId = serverPageRequestTxn.contexts?.trace?.span_id; + expect(clientPageloadTxn.contexts?.trace?.parent_span_id).toEqual(serverPageRequestSpanId); + expect(serverRequestHTTPClientSpan?.parent_span_id).toEqual(serverPageRequestSpanId); + + // serverHTTPServerRequest's parent is serverRequestHTTPClient + expect(serverHTTPServerRequestTxn.contexts?.trace?.parent_span_id).toEqual(serverRequestHTTPClientSpan?.span_id); + }); + + test('sends parametrized pageload, server and API request transaction names', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; + }); + + await page.goto('/user-page/myUsername123'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; + + const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( + span => span.op === 'http.client' && span.description?.includes('/api/user/'), + ); + + // Client pageload transaction - actual URL with pageload operation + expect(clientPageloadTxn).toMatchObject({ + transaction: '/user-page/myUsername123', // todo: parametrize + transaction_info: { source: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + // Server page request transaction - parametrized transaction name with actual URL in data + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /user-page/[userId]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/user-page/myUsername123'), + }, + }, + }, + request: { url: expect.stringContaining('/user-page/myUsername123') }, + }); + + // HTTP client span - actual API URL with client operation + expect(serverRequestHTTPClientSpan).toMatchObject({ + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized + data: { + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.node_fetch', + 'url.full': expect.stringContaining('/api/user/myUsername123.json'), + 'url.path': '/api/user/myUsername123.json', + url: expect.stringContaining('/api/user/myUsername123.json'), + }, + }); + + // Server HTTP request transaction - should be parametrized + expect(serverHTTPServerRequestTxn).toMatchObject({ + transaction: 'GET /api/user/myUsername123.json', // todo: parametrize + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/api/user/myUsername123.json'), + }, + }, + }, + request: { url: expect.stringContaining('/api/user/myUsername123.json') }, + }); + }); + + test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('/catchAll/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false; + }); + + await page.goto('/catchAll/hell0/whatever-do'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/catchAll/hell0/whatever-do', // todo: parametrize + transaction_info: { source: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /catchAll/[path]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/catchAll/hell0/whatever-do'), + }, + }, + }, + request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro new file mode 100644 index 000000000000..bb225c166241 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/catchAll/[...path].astro @@ -0,0 +1,12 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +const params = Astro.params; + +--- + + +

params: {params}

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro new file mode 100644 index 000000000000..8260e632c07b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-5/src/pages/user-page/settings.astro @@ -0,0 +1,7 @@ +--- +import Layout from '../../layouts/Layout.astro'; +--- + + +

User Settings

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index 36f32cd5dc0e..8267adcb4ea9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -233,7 +233,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { expect(serverRequestHTTPClientSpan).toMatchObject({ op: 'http.client', origin: 'auto.http.otel.node_fetch', - description: 'GET http://localhost:3030/api/user/myUsername123.json', // todo: parametrize (this is just a span though - no transaction) + description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized data: { 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.otel.node_fetch', @@ -243,9 +243,9 @@ test.describe('nested SSR routes (client, server, server request)', () => { }, }); - // Server HTTP request transaction - should be parametrized (todo: currently not parametrized) + // Server HTTP request transaction expect(serverHTTPServerRequestTxn).toMatchObject({ - transaction: 'GET /api/user/myUsername123.json', // todo: should be parametrized to 'GET /api/user/[userId].json' + transaction: 'GET /api/user/[userId].json', transaction_info: { source: 'route' }, contexts: { trace: { @@ -262,4 +262,105 @@ test.describe('nested SSR routes (client, server, server request)', () => { request: { url: expect.stringContaining('/api/user/myUsername123.json') }, }); }); + + test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction?.startsWith('/catchAll/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false; + }); + + await page.goto('/catchAll/hell0/whatever-do'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/catchAll/hell0/whatever-do', // todo: parametrize to '/catchAll/[...path]' + transaction_info: { source: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /catchAll/[...path]', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/catchAll/hell0/whatever-do'), + }, + }, + }, + request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') }, + }); + }); +}); + +// Case for `user-page/[id]` vs. `user-page/settings` static routes +test.describe('parametrized vs static paths', () => { + test('should use static route name for static route in parametrized path', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + await page.goto('/user-page/settings'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/user-page/settings', + transaction_info: { source: 'url' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.browser', + 'sentry.source': 'url', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /user-page/settings', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/user-page/settings'), + }, + }, + }, + request: { url: expect.stringContaining('/user-page/settings') }, + }); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts index fc396999d76e..dda88afe4714 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -63,7 +63,7 @@ test.describe('tracing in static routes with server islands', () => { ]), ); - expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Fserver-island'); // URL-encoded for 'GET /server-island' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise; diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts index 9db35c72a47d..90b26fb645e9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -53,7 +53,7 @@ test.describe('tracing in static/pre-rendered routes', () => { type: 'transaction', }); - expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static'); // URL-encoded for 'GET /test-static' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 28092cad84be..69564fcc6535 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -1,14 +1,18 @@ -import { consoleSandbox } from '@sentry/core'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { consoleSandbox, debug } from '@sentry/core'; import { sentryVitePlugin } from '@sentry/vite-plugin'; -import type { AstroConfig, AstroIntegration } from 'astro'; +import type { AstroConfig, AstroIntegration, RoutePart } from 'astro'; import * as fs from 'fs'; import * as path from 'path'; import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets'; -import type { SentryOptions } from './types'; +import type { IntegrationResolvedRoute, SentryOptions } from './types'; const PKG_NAME = '@sentry/astro'; export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { + let sentryServerInitPath: string | undefined; + let didSaveRouteData = false; + return { name: PKG_NAME, hooks: { @@ -134,6 +138,8 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { injectScript('page-ssr', buildServerSnippet(options || {})); } + sentryServerInitPath = pathToServerInit; + // Prevent Sentry from being externalized for SSR. // Cloudflare like environments have Node.js APIs are available under `node:` prefix. // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ @@ -165,6 +171,36 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { }); } }, + + // @ts-expect-error - This hook is available in Astro 5+ + 'astro:routes:resolved': ({ routes }: { routes: IntegrationResolvedRoute[] }) => { + if (!sentryServerInitPath || didSaveRouteData) { + return; + } + + try { + const serverInitContent = readFileSync(sentryServerInitPath, 'utf8'); + + const updatedServerInitContent = `${serverInitContent}\nglobalThis["__sentryRouteInfo"] = ${JSON.stringify( + routes.map(route => { + return { + ...route, + patternCaseSensitive: joinRouteSegments(route.segments), // Store parametrized routes with correct casing on `globalThis` to be able to use them on the server during runtime + patternRegex: route.patternRegex.source, // using `source` to be able to serialize the regex + }; + }), + null, + 2, + )};`; + + writeFileSync(sentryServerInitPath, updatedServerInitContent, 'utf8'); + + didSaveRouteData = true; // Prevents writing the file multiple times during runtime + debug.log('Successfully added route pattern information to Sentry config file:', sentryServerInitPath); + } catch (error) { + debug.warn(`Failed to write to Sentry config file at ${sentryServerInitPath}:`, error); + } + }, }, }; }; @@ -271,3 +307,18 @@ export function getUpdatedSourceMapSettings( return { previousUserSourceMapSetting, updatedSourceMapSetting }; } + +/** + * Join Astro route segments into a case-sensitive single path string. + * + * Astro lowercases the parametrized route. Joining segments manually is recommended to get the correct casing of the routes. + * Recommendation in comment: https://github.com/withastro/astro/issues/13885#issuecomment-2934203029 + * Function Reference: https://github.com/joanrieu/astro-typed-links/blob/b3dc12c6fe8d672a2bc2ae2ccc57c8071bbd09fa/package/src/integration.ts#L16 + */ +function joinRouteSegments(segments: RoutePart[][]): string { + const parthArray = segments.map(segment => + segment.map(routePart => (routePart.dynamic ? `[${routePart.content}]` : routePart.content)).join(''), + ); + + return `/${parthArray.join('/')}`; +} diff --git a/packages/astro/src/integration/types.ts b/packages/astro/src/integration/types.ts index 08a8635889fe..aed2b7e1d193 100644 --- a/packages/astro/src/integration/types.ts +++ b/packages/astro/src/integration/types.ts @@ -1,4 +1,5 @@ import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; +import type { RouteData } from 'astro'; type SdkInitPaths = { /** @@ -224,3 +225,27 @@ export type SentryOptions = SdkInitPaths & debug?: boolean; // eslint-disable-next-line deprecation/deprecation } & DeprecatedRuntimeOptions; + +/** + * Routes inside 'astro:routes:resolved' hook (Astro v5+) + * + * Inline type for official `IntegrationResolvedRoute`. + * The type includes more properties, but we only need some of them. + * + * @see https://github.com/withastro/astro/blob/04e60119afee668264a2ff6665c19a32150f4c91/packages/astro/src/types/public/integrations.ts#L287 + */ +export type IntegrationResolvedRoute = { + isPrerendered: RouteData['prerender']; + pattern: RouteData['route']; + patternRegex: RouteData['pattern']; + segments: RouteData['segments']; +}; + +/** + * Internal type for Astro routes, as we store an additional `patternCaseSensitive` property alongside the + * lowercased parametrized `pattern` of each Astro route. + */ +export type ResolvedRouteWithCasedPattern = IntegrationResolvedRoute & { + patternRegex: string; // RegEx gets stringified + patternCaseSensitive: string; +}; diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index fb2f2e572fa4..ec9b8cd2f82f 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -23,6 +23,7 @@ import { withIsolationScope, } from '@sentry/node'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; +import type { ResolvedRouteWithCasedPattern } from '../integration/types'; type MiddlewareOptions = { /** @@ -95,6 +96,9 @@ async function instrumentRequest( addNonEnumerableProperty(locals, '__sentry_wrapped__', true); } + const storedBuildTimeRoutes = (globalThis as unknown as { __sentryRouteInfo?: ResolvedRouteWithCasedPattern[] }) + ?.__sentryRouteInfo; + const isDynamicPageRequest = checkIsDynamicPageRequest(ctx); const request = ctx.request; @@ -128,8 +132,15 @@ async function instrumentRequest( } try { - const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); - const source = interpolatedRoute ? 'route' : 'url'; + // `routePattern` is available after Astro 5 + const contextWithRoutePattern = ctx as Parameters[0] & { routePattern?: string }; + const rawRoutePattern = contextWithRoutePattern.routePattern; + const foundRoute = storedBuildTimeRoutes?.find(route => route.pattern === rawRoutePattern); + + const parametrizedRoute = + foundRoute?.patternCaseSensitive || interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); + + const source = parametrizedRoute ? 'route' : 'url'; // storing res in a variable instead of directly returning is necessary to // invoke the catch block if next() throws @@ -148,12 +159,12 @@ async function instrumentRequest( attributes['http.fragment'] = ctx.url.hash; } - isolationScope?.setTransactionName(`${method} ${interpolatedRoute || ctx.url.pathname}`); + isolationScope?.setTransactionName(`${method} ${parametrizedRoute || ctx.url.pathname}`); const res = await startSpan( { attributes, - name: `${method} ${interpolatedRoute || ctx.url.pathname}`, + name: `${method} ${parametrizedRoute || ctx.url.pathname}`, op: 'http.server', }, async span => { From 7c2e347339ec019d59bc857563bf4f8721dac580 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 24 Jul 2025 11:56:07 +0200 Subject: [PATCH 08/14] test(v9/remix): Fix integration test flakes (#17144) Backport of https://github.com/getsentry/sentry-javascript/pull/17093 Co-authored-by: Onur Temizkan --- packages/remix/test/integration/app/root.tsx | 4 +-- .../test/client/root-loader.test.ts | 30 +++++++------------ 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/remix/test/integration/app/root.tsx b/packages/remix/test/integration/app/root.tsx index 1b8e5e39e8f5..c1d0bf218baa 100644 --- a/packages/remix/test/integration/app/root.tsx +++ b/packages/remix/test/integration/app/root.tsx @@ -48,9 +48,9 @@ export const loader: LoaderFunction = async ({ request }) => { case 'returnRedirect': return redirect('/?type=plain'); case 'throwRedirectToExternal': - throw redirect('https://example.com'); + throw redirect(`https://docs.sentry.io`); case 'returnRedirectToExternal': - return redirect('https://example.com'); + return redirect('https://docs.sentry.io'); default: { return {}; } diff --git a/packages/remix/test/integration/test/client/root-loader.test.ts b/packages/remix/test/integration/test/client/root-loader.test.ts index e9273fbd6caa..fc557509e941 100644 --- a/packages/remix/test/integration/test/client/root-loader.test.ts +++ b/packages/remix/test/integration/test/client/root-loader.test.ts @@ -1,4 +1,4 @@ -import { type Page, expect, test } from '@playwright/test'; +import { type Page, expect, test, chromium } from '@playwright/test'; async function getRouteData(page: Page): Promise { return page.evaluate('window.__remixContext.state.loaderData').catch(err => { @@ -22,7 +22,6 @@ async function extractTraceAndBaggageFromMeta( test('should inject `sentry-trace` and `baggage` into root loader returning an empty object.', async ({ page }) => { await page.goto('/?type=empty'); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); expect(sentryTrace).toMatch(/.+/); @@ -38,7 +37,6 @@ test('should inject `sentry-trace` and `baggage` into root loader returning an e test('should inject `sentry-trace` and `baggage` into root loader returning a plain object.', async ({ page }) => { await page.goto('/?type=plain'); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); expect(sentryTrace).toMatch(/.+/); @@ -56,7 +54,6 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a pl test('should inject `sentry-trace` and `baggage` into root loader returning a `JSON response`.', async ({ page }) => { await page.goto('/?type=json'); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); expect(sentryTrace).toMatch(/.+/); @@ -74,7 +71,6 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a `J test('should inject `sentry-trace` and `baggage` into root loader returning a deferred response', async ({ page }) => { await page.goto('/?type=defer'); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); expect(sentryTrace).toMatch(/.+/); @@ -90,7 +86,6 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a de test('should inject `sentry-trace` and `baggage` into root loader returning `null`.', async ({ page }) => { await page.goto('/?type=null'); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); expect(sentryTrace).toMatch(/.+/); @@ -106,7 +101,6 @@ test('should inject `sentry-trace` and `baggage` into root loader returning `nul test('should inject `sentry-trace` and `baggage` into root loader returning `undefined`.', async ({ page }) => { await page.goto('/?type=undefined'); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); expect(sentryTrace).toMatch(/.+/); @@ -124,12 +118,11 @@ test('should inject `sentry-trace` and `baggage` into root loader throwing a red page, }) => { await page.goto('/?type=throwRedirect'); + const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); // We should be successfully redirected to the path. expect(page.url()).toEqual(expect.stringContaining('/?type=plain')); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toMatch(/.+/); expect(sentryBaggage).toMatch(/.+/); @@ -143,14 +136,14 @@ test('should inject `sentry-trace` and `baggage` into root loader throwing a red test('should inject `sentry-trace` and `baggage` into root loader returning a redirection to valid path.', async ({ page, + baseURL, }) => { - await page.goto('/?type=returnRedirect'); + await page.goto(`${baseURL}/?type=returnRedirect`); + const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); // We should be successfully redirected to the path. expect(page.url()).toEqual(expect.stringContaining('/?type=plain')); - const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toMatch(/.+/); expect(sentryBaggage).toMatch(/.+/); @@ -162,11 +155,10 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a re }); }); -test('should return redirect to an external path with no baggage and trace injected.', async ({ page }) => { - await page.goto('/?type=returnRedirectToExternal'); +test('should return redirect to an external path with no baggage and trace injected.', async ({ page, baseURL }) => { + await page.goto(`${baseURL}/?type=returnRedirectToExternal`); - // We should be successfully redirected to the external path. - expect(page.url()).toEqual(expect.stringContaining('https://example.com')); + expect(page.url()).toEqual(expect.stringContaining('docs.sentry.io')); const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); @@ -174,11 +166,11 @@ test('should return redirect to an external path with no baggage and trace injec expect(sentryBaggage).toBeUndefined(); }); -test('should throw redirect to an external path with no baggage and trace injected.', async ({ page }) => { - await page.goto('/?type=throwRedirectToExternal'); +test('should throw redirect to an external path with no baggage and trace injected.', async ({ page, baseURL }) => { + await page.goto(`${baseURL}/?type=throwRedirectToExternal`); // We should be successfully redirected to the external path. - expect(page.url()).toEqual(expect.stringContaining('https://example.com')); + expect(page.url()).toEqual(expect.stringContaining('docs.sentry.io')); const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); From d6eaacf34ba10e9a2ef4dc94a55439f088777962 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:00:47 +0200 Subject: [PATCH 09/14] feat(v9/astro): Parametrize routes on client-side (#17143) Backport of https://github.com/getsentry/sentry-javascript/pull/17133 --- .../src/pages/user-page/settings.astro | 10 ++ .../astro-4/tests/tracing.dynamic.test.ts | 92 +++++++++++++++---- .../astro-4/tests/tracing.static.test.ts | 10 +- .../astro-5/tests/tracing.dynamic.test.ts | 42 +++++---- .../tests/tracing.serverIslands.test.ts | 8 +- .../astro-5/tests/tracing.static.test.ts | 8 +- .../src/client/browserTracingIntegration.ts | 52 +++++++++++ packages/astro/src/client/sdk.ts | 7 +- packages/astro/src/debug-build.ts | 8 ++ packages/astro/src/index.client.ts | 3 + packages/astro/src/server/middleware.ts | 46 ++++++---- packages/astro/test/server/middleware.test.ts | 7 ++ 12 files changed, 218 insertions(+), 75 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/settings.astro create mode 100644 packages/astro/src/client/browserTracingIntegration.ts create mode 100644 packages/astro/src/debug-build.ts diff --git a/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/settings.astro b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/settings.astro new file mode 100644 index 000000000000..5a46ac891a24 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-4/src/pages/user-page/settings.astro @@ -0,0 +1,10 @@ +--- +import Layout from '../../layouts/Layout.astro'; + +export const prerender = false; + +--- + + +

User Settings

+
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts index 2699e827718e..07e0467382da 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts @@ -30,11 +30,11 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { trace: { data: expect.objectContaining({ 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }), op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', span_id: expect.stringMatching(/[a-f0-9]{16}/), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -55,9 +55,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: '/test-ssr', - transaction_info: { - source: 'url', - }, + transaction_info: { source: 'route' }, type: 'transaction', }); @@ -113,9 +111,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { start_timestamp: expect.any(Number), timestamp: expect.any(Number), transaction: 'GET /test-ssr', - transaction_info: { - source: 'route', - }, + transaction_info: { source: 'route' }, type: 'transaction', }); }); @@ -194,18 +190,21 @@ test.describe('nested SSR routes (client, server, server request)', () => { span => span.op === 'http.client' && span.description?.includes('/api/user/'), ); + const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); + expect(routeNameMetaContent).toBe('%2Fuser-page%2F%5BuserId%5D'); + // Client pageload transaction - actual URL with pageload operation expect(clientPageloadTxn).toMatchObject({ - transaction: '/user-page/myUsername123', // todo: parametrize - transaction_info: { source: 'url' }, + transaction: '/user-page/[userId]', + transaction_info: { source: 'route' }, contexts: { trace: { op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', data: { 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }, }, }, @@ -275,20 +274,23 @@ test.describe('nested SSR routes (client, server, server request)', () => { await page.goto('/catchAll/hell0/whatever-do'); + const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); + expect(routeNameMetaContent).toBe('%2FcatchAll%2F%5Bpath%5D'); + const clientPageloadTxn = await clientPageloadTxnPromise; const serverPageRequestTxn = await serverPageRequestTxnPromise; expect(clientPageloadTxn).toMatchObject({ - transaction: '/catchAll/hell0/whatever-do', // todo: parametrize - transaction_info: { source: 'url' }, + transaction: '/catchAll/[path]', + transaction_info: { source: 'route' }, contexts: { trace: { op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', data: { 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }, }, }, @@ -313,3 +315,55 @@ test.describe('nested SSR routes (client, server, server request)', () => { }); }); }); + +// Case for `user-page/[id]` vs. `user-page/settings` static routes +test.describe('parametrized vs static paths', () => { + test('should use static route name for static route in parametrized path', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('/user-page/') ?? false; + }); + + const serverPageRequestTxnPromise = waitForTransaction('astro-4', txnEvent => { + return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; + }); + + await page.goto('/user-page/settings'); + + const clientPageloadTxn = await clientPageloadTxnPromise; + const serverPageRequestTxn = await serverPageRequestTxnPromise; + + expect(clientPageloadTxn).toMatchObject({ + transaction: '/user-page/settings', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.astro', + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', + }, + }, + }, + }); + + expect(serverPageRequestTxn).toMatchObject({ + transaction: 'GET /user-page/settings', + transaction_info: { source: 'route' }, + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.astro', + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.astro', + 'sentry.source': 'route', + url: expect.stringContaining('/user-page/settings'), + }, + }, + }, + request: { url: expect.stringContaining('/user-page/settings') }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts index c04bbb568f2e..30bcbee1a026 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts @@ -35,11 +35,11 @@ test.describe('tracing in static/pre-rendered routes', () => { trace: { data: expect.objectContaining({ 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }), op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', parent_span_id: metaParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: metaTraceId, @@ -48,12 +48,12 @@ test.describe('tracing in static/pre-rendered routes', () => { platform: 'javascript', transaction: '/test-static', transaction_info: { - source: 'url', + source: 'route', }, type: 'transaction', }); - expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static%2F'); // URL-encoded for 'GET /test-static/' + expect(baggageMetaTagContent).toContain('sentry-transaction=GET%20%2Ftest-static'); // URL-encoded for 'GET /test-static' expect(baggageMetaTagContent).toContain('sentry-sampled=true'); await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index 8267adcb4ea9..b7dda807c65c 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -30,11 +30,11 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { trace: { data: expect.objectContaining({ 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }), op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', span_id: expect.stringMatching(/[a-f0-9]{16}/), parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: expect.stringMatching(/[a-f0-9]{32}/), @@ -56,7 +56,7 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { timestamp: expect.any(Number), transaction: '/test-ssr', transaction_info: { - source: 'url', + source: 'route', }, type: 'transaction', }); @@ -193,18 +193,21 @@ test.describe('nested SSR routes (client, server, server request)', () => { span => span.op === 'http.client' && span.description?.includes('/api/user/'), ); + const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); + expect(routeNameMetaContent).toBe('%2Fuser-page%2F%5BuserId%5D'); + // Client pageload transaction - actual URL with pageload operation expect(clientPageloadTxn).toMatchObject({ - transaction: '/user-page/myUsername123', // todo: parametrize to '/user-page/[userId]' - transaction_info: { source: 'url' }, + transaction: '/user-page/[userId]', + transaction_info: { source: 'route' }, contexts: { trace: { op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', data: { 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }, }, }, @@ -274,20 +277,23 @@ test.describe('nested SSR routes (client, server, server request)', () => { await page.goto('/catchAll/hell0/whatever-do'); + const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); + expect(routeNameMetaContent).toBe('%2FcatchAll%2F%5B...path%5D'); + const clientPageloadTxn = await clientPageloadTxnPromise; const serverPageRequestTxn = await serverPageRequestTxnPromise; expect(clientPageloadTxn).toMatchObject({ - transaction: '/catchAll/hell0/whatever-do', // todo: parametrize to '/catchAll/[...path]' - transaction_info: { source: 'url' }, + transaction: '/catchAll/[...path]', + transaction_info: { source: 'route' }, contexts: { trace: { op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', data: { 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }, }, }, @@ -331,15 +337,15 @@ test.describe('parametrized vs static paths', () => { expect(clientPageloadTxn).toMatchObject({ transaction: '/user-page/settings', - transaction_info: { source: 'url' }, + transaction_info: { source: 'route' }, contexts: { trace: { op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', data: { 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }, }, }, diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts index dda88afe4714..202051e7a57e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -32,11 +32,11 @@ test.describe('tracing in static routes with server islands', () => { trace: { data: expect.objectContaining({ 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }), op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', parent_span_id: metaParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: metaTraceId, @@ -45,7 +45,7 @@ test.describe('tracing in static routes with server islands', () => { platform: 'javascript', transaction: '/server-island', transaction_info: { - source: 'url', + source: 'route', }, type: 'transaction', }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts index 90b26fb645e9..7593d6823d9b 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -35,11 +35,11 @@ test.describe('tracing in static/pre-rendered routes', () => { trace: { data: expect.objectContaining({ 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.browser', - 'sentry.source': 'url', + 'sentry.origin': 'auto.pageload.astro', + 'sentry.source': 'route', }), op: 'pageload', - origin: 'auto.pageload.browser', + origin: 'auto.pageload.astro', parent_span_id: metaParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), trace_id: metaTraceId, @@ -48,7 +48,7 @@ test.describe('tracing in static/pre-rendered routes', () => { platform: 'javascript', transaction: '/test-static', transaction_info: { - source: 'url', + source: 'route', }, type: 'transaction', }); diff --git a/packages/astro/src/client/browserTracingIntegration.ts b/packages/astro/src/client/browserTracingIntegration.ts new file mode 100644 index 000000000000..7f8576671635 --- /dev/null +++ b/packages/astro/src/client/browserTracingIntegration.ts @@ -0,0 +1,52 @@ +import { browserTracingIntegration as originalBrowserTracingIntegration, WINDOW } from '@sentry/browser'; +import type { Integration, TransactionSource } from '@sentry/core'; +import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; + +/** + * Returns the value of a meta-tag + */ +function getMetaContent(metaName: string): string | undefined { + const optionalDocument = WINDOW.document as (typeof WINDOW)['document'] | undefined; + const metaTag = optionalDocument?.querySelector(`meta[name=${metaName}]`); + return metaTag?.getAttribute('content') || undefined; +} + +/** + * A custom browser tracing integrations for Astro. + */ +export function browserTracingIntegration( + options: Parameters[0] = {}, +): Integration { + const integration = originalBrowserTracingIntegration(options); + + return { + ...integration, + setup(client) { + // Original integration setup call + integration.setup?.(client); + + client.on('afterStartPageLoadSpan', pageLoadSpan => { + const routeNameFromMetaTags = getMetaContent('sentry-route-name'); + + if (routeNameFromMetaTags) { + let decodedRouteName; + try { + decodedRouteName = decodeURIComponent(routeNameFromMetaTags); + } catch { + // We ignore errors here, e.g. if the value cannot be URL decoded. + return; + } + + DEBUG_BUILD && debug.log(`[Tracing] Using route name from Sentry HTML meta-tag: ${decodedRouteName}`); + + pageLoadSpan.updateName(decodedRouteName); + pageLoadSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' as TransactionSource, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.astro', + }); + } + }); + }, + }; +} diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts index f04725d1ef1e..21c5770f255f 100644 --- a/packages/astro/src/client/sdk.ts +++ b/packages/astro/src/client/sdk.ts @@ -1,11 +1,8 @@ import type { BrowserOptions } from '@sentry/browser'; -import { - browserTracingIntegration, - getDefaultIntegrations as getBrowserDefaultIntegrations, - init as initBrowserSdk, -} from '@sentry/browser'; +import { getDefaultIntegrations as getBrowserDefaultIntegrations, init as initBrowserSdk } from '@sentry/browser'; import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; +import { browserTracingIntegration } from './browserTracingIntegration'; // Tree-shakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; diff --git a/packages/astro/src/debug-build.ts b/packages/astro/src/debug-build.ts new file mode 100644 index 000000000000..60aa50940582 --- /dev/null +++ b/packages/astro/src/debug-build.ts @@ -0,0 +1,8 @@ +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/astro/src/index.client.ts b/packages/astro/src/index.client.ts index 2b85c05c3af1..55a6c8d3915b 100644 --- a/packages/astro/src/index.client.ts +++ b/packages/astro/src/index.client.ts @@ -1,3 +1,6 @@ export * from '@sentry/browser'; +// Override the browserTracingIntegration with the custom Astro version +export { browserTracingIntegration } from './client/browserTracingIntegration'; + export { init } from './client/sdk'; diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index ec9b8cd2f82f..9f04d5427fcf 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -213,7 +213,7 @@ async function instrumentRequest( try { for await (const chunk of bodyReporter()) { const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); - const modifiedHtml = addMetaTagToHead(html); + const modifiedHtml = addMetaTagToHead(html, parametrizedRoute); controller.enqueue(new TextEncoder().encode(modifiedHtml)); } } catch (e) { @@ -253,11 +253,13 @@ async function instrumentRequest( * This function optimistically assumes that the HTML coming in chunks will not be split * within the tag. If this still happens, we simply won't replace anything. */ -function addMetaTagToHead(htmlChunk: string): string { +function addMetaTagToHead(htmlChunk: string, parametrizedRoute?: string): string { if (typeof htmlChunk !== 'string') { return htmlChunk; } - const metaTags = getTraceMetaTags(); + const metaTags = parametrizedRoute + ? `${getTraceMetaTags()}\n\n` + : getTraceMetaTags(); if (!metaTags) { return htmlChunk; @@ -317,26 +319,30 @@ export function interpolateRouteFromUrlAndParams( return acc.replace(key, `[${valuesToMultiSegmentParams[key]}]`); }, decodedUrlPathname); - return urlWithReplacedMultiSegmentParams - .split('/') - .map(segment => { - if (!segment) { - return ''; - } + return ( + urlWithReplacedMultiSegmentParams + .split('/') + .map(segment => { + if (!segment) { + return ''; + } - if (valuesToParams[segment]) { - return replaceWithParamName(segment); - } + if (valuesToParams[segment]) { + return replaceWithParamName(segment); + } - // astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/ - const segmentParts = segment.split('-'); - if (segmentParts.length > 1) { - return segmentParts.map(part => replaceWithParamName(part)).join('-'); - } + // astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/ + const segmentParts = segment.split('-'); + if (segmentParts.length > 1) { + return segmentParts.map(part => replaceWithParamName(part)).join('-'); + } - return segment; - }) - .join('/'); + return segment; + }) + .join('/') + // Remove trailing slash (only if it's not the only segment) + .replace(/^(.+?)\/$/, '$1') + ); } function tryDecodeUrl(url: string): string | undefined { diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index e8c16fa570d3..6430a5f47eb7 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -482,4 +482,11 @@ describe('interpolateRouteFromUrlAndParams', () => { const expectedRoute = '/usernames/user'; expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute); }); + + it('removes trailing slashes from the route', () => { + const rawUrl = '/users/123/'; + const params = { id: '123' }; + const expectedRoute = '/users/[id]'; + expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute); + }); }); From 0144544b5f13b4989f5917822fbf27e6af2c9e11 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Thu, 24 Jul 2025 12:22:09 +0200 Subject: [PATCH 10/14] fix(v9/node): Ensure tool errors for `vercelAiIntegration` have correct trace (#17142) Backport of https://github.com/getsentry/sentry-javascript/pull/17132 Fixes https://github.com/getsentry/sentry-javascript/issues/17108 --- .../aws-lambda-layer-cjs/package.json | 3 +- .../handle-error-tracesSampleRate-0/server.ts | 2 +- .../handle-error-tracesSampleRate-0/test.ts | 1 - .../suites/express/handle-error/server.ts | 21 ++ .../suites/express/handle-error/test.ts | 44 ++++ .../scenario-with-span-ended.ts | 46 ++++ .../scenario-with-span.ts | 14 + .../onUnhandledRejectionIntegration/test.ts | 55 ++++ .../tracing/vercelai/instrument-with-pii.mjs | 2 +- .../suites/tracing/vercelai/instrument.mjs | 2 +- .../scenario-error-in-tool-express.mjs | 50 ++++ .../vercelai/scenario-error-in-tool.mjs | 40 +++ .../suites/tracing/vercelai/test.ts | 245 +++++++++++++++++- .../src/integrations/onunhandledrejection.ts | 35 ++- .../tracing/vercelai/instrumentation.ts | 25 +- 15 files changed, 564 insertions(+), 21 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express/handle-error/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express/handle-error/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool.mjs diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json index 1d98acc92859..25489cf0a35e 100644 --- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json @@ -1,7 +1,8 @@ { - "name": "node-express-app", + "name": "aws-lambda-layer-cjs", "version": "1.0.0", "private": true, + "type": "commonjs", "scripts": { "start": "node src/run.js", "test": "playwright test", diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/server.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/server.ts index 323093ce38e0..329d658d905a 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/server.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/server.ts @@ -5,7 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, - tracesSampleRate: 1, + tracesSampleRate: 0, }); import express from 'express'; diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts index b6bc5de97cdb..1f434ebc0971 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts @@ -7,7 +7,6 @@ afterAll(() => { test('should capture and send Express controller error with txn name if tracesSampleRate is 0', async () => { const runner = createRunner(__dirname, 'server.ts') - .ignore('transaction') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/server.ts b/dev-packages/node-integration-tests/suites/express/handle-error/server.ts new file mode 100644 index 000000000000..ba8fb32cc108 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error/server.ts @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + transport: loggingTransport, +}); + +import express from 'express'; + +const app = express(); + +app.get('/test/express/:id', req => { + throw new Error(`test_error with id ${req.params.id}`); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts new file mode 100644 index 000000000000..0db624160959 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts @@ -0,0 +1,44 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture and send Express controller error with txn name if tracesSampleRate is 1', async () => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + transaction: { + transaction: 'GET /test/express/:id', + }, + }) + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'test_error with id 123', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + transaction: 'GET /test/express/:id', + }, + }) + .start(); + runner.makeRequest('get', '/test/express/123', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts new file mode 100644 index 000000000000..72d83d70ec72 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span-ended.ts @@ -0,0 +1,46 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + transport: loggingTransport, +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +recordSpan(async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + doSomething(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + doSomethingWithError(); +}); + +async function doSomething(): Promise { + return Promise.resolve(); +} + +async function doSomethingWithError(): Promise { + await new Promise(resolve => setTimeout(resolve, 100)); + throw new Error('test error'); +} + +// Duplicating some code from vercel-ai to verify how things work in more complex/weird scenarios +function recordSpan(fn: (span: unknown) => Promise): Promise { + return Sentry.startSpanManual({ name: 'test-span' }, async span => { + try { + const result = await fn(span); + span.end(); + return result; + } catch (error) { + try { + span.setStatus({ code: 2 }); + } finally { + // always stop the span when there is an error: + span.end(); + } + + throw error; + } + }); +} diff --git a/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts new file mode 100644 index 000000000000..edff30f114ca --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-with-span.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + transport: loggingTransport, +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test-span' }, async () => { + throw new Error('test error'); +}); 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 2f4a22c835a4..468e66a058ca 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 @@ -1,3 +1,4 @@ +import type { Event } from '@sentry/node'; import * as childProcess from 'child_process'; import * as path from 'path'; import { afterAll, describe, expect, test } from 'vitest'; @@ -123,4 +124,58 @@ test rejection`); .start() .completed(); }); + + test('handles unhandled rejection in spans', async () => { + let transactionEvent: Event | undefined; + let errorEvent: Event | undefined; + + await createRunner(__dirname, 'scenario-with-span.ts') + .expect({ + transaction: transaction => { + transactionEvent = transaction; + }, + }) + .expect({ + event: event => { + errorEvent = event; + }, + }) + .start() + .completed(); + + expect(transactionEvent).toBeDefined(); + expect(errorEvent).toBeDefined(); + + expect(transactionEvent!.transaction).toBe('test-span'); + + 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('handles unhandled rejection in spans that are ended early', async () => { + let transactionEvent: Event | undefined; + let errorEvent: Event | undefined; + + await createRunner(__dirname, 'scenario-with-span-ended.ts') + .expect({ + transaction: transaction => { + transactionEvent = transaction; + }, + }) + .expect({ + event: event => { + errorEvent = event; + }, + }) + .start() + .completed(); + + expect(transactionEvent).toBeDefined(); + expect(errorEvent).toBeDefined(); + + expect(transactionEvent!.transaction).toBe('test-span'); + + expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id); + expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id); + }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs index d69f7dca5feb..b798e21228f5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument-with-pii.mjs @@ -7,5 +7,5 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: true, transport: loggingTransport, - integrations: [Sentry.vercelAIIntegration({ force: true })], + integrations: [Sentry.vercelAIIntegration()], }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs index e4cd7b9cabd7..5e898ee1949d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/instrument.mjs @@ -6,5 +6,5 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, - integrations: [Sentry.vercelAIIntegration({ force: true })], + integrations: [Sentry.vercelAIIntegration()], }); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs new file mode 100644 index 000000000000..82bfe3c35445 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs @@ -0,0 +1,50 @@ +import * as Sentry from '@sentry/node'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import { generateText } from 'ai'; +import { MockLanguageModelV1 } from 'ai/test'; +import express from 'express'; +import { z } from 'zod'; + +const app = express(); + +app.get('/test/error-in-tool', async (_req, res, next) => { + Sentry.setTag('test-tag', 'test-value'); + + try { + await generateText({ + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'tool-calls', + usage: { promptTokens: 15, completionTokens: 25 }, + text: 'Tool call completed!', + toolCalls: [ + { + toolCallType: 'function', + toolCallId: 'call-1', + toolName: 'getWeather', + args: '{ "location": "San Francisco" }', + }, + ], + }), + }), + tools: { + getWeather: { + parameters: z.object({ location: z.string() }), + execute: async () => { + throw new Error('Error in tool'); + }, + }, + }, + prompt: 'What is the weather in San Francisco?', + }); + } catch (error) { + next(error); + return; + } + + res.send({ message: 'OK' }); +}); +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool.mjs new file mode 100644 index 000000000000..4185d972da4d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool.mjs @@ -0,0 +1,40 @@ +import * as Sentry from '@sentry/node'; +import { generateText } from 'ai'; +import { MockLanguageModelV1 } from 'ai/test'; +import { z } from 'zod'; + +async function run() { + Sentry.setTag('test-tag', 'test-value'); + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + await generateText({ + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'tool-calls', + usage: { promptTokens: 15, completionTokens: 25 }, + text: 'Tool call completed!', + toolCalls: [ + { + toolCallType: 'function', + toolCallId: 'call-1', + toolName: 'getWeather', + args: '{ "location": "San Francisco" }', + }, + ], + }), + }), + tools: { + getWeather: { + parameters: z.object({ location: z.string() }), + execute: async () => { + throw new Error('Error in tool'); + }, + }, + }, + prompt: 'What is the weather in San Francisco?', + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index f9b853aa4946..5353f53f42e3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -1,7 +1,7 @@ +import type { Event } from '@sentry/node'; import { afterAll, describe, expect } from 'vitest'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -// `ai` SDK only support Node 18+ describe('Vercel AI integration', () => { afterAll(() => { cleanupChildProcesses(); @@ -416,4 +416,247 @@ describe('Vercel AI integration', () => { await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); }); }); + + createEsmAndCjsTests(__dirname, 'scenario-error-in-tool.mjs', 'instrument.mjs', (createRunner, test) => { + test('captures error in tool', async () => { + const expectedTransaction = { + transaction: 'main', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'vercel.ai.model.id': 'mock-model-id', + 'vercel.ai.model.provider': 'mock-provider', + 'vercel.ai.operationId': 'ai.generateText', + 'vercel.ai.pipeline.name': 'generateText', + 'vercel.ai.settings.maxRetries': 2, + 'vercel.ai.settings.maxSteps': 1, + 'vercel.ai.streaming': false, + 'gen_ai.response.model': 'mock-model-id', + 'operation.name': 'ai.generateText', + 'sentry.op': 'gen_ai.invoke_agent', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generateText', + op: 'gen_ai.invoke_agent', + origin: 'auto.vercelai.otel', + status: 'unknown_error', + }), + expect.objectContaining({ + data: { + 'vercel.ai.model.id': 'mock-model-id', + 'vercel.ai.model.provider': 'mock-provider', + 'vercel.ai.operationId': 'ai.generateText.doGenerate', + 'vercel.ai.pipeline.name': 'generateText.doGenerate', + 'vercel.ai.response.finishReason': 'tool-calls', + 'vercel.ai.response.id': expect.any(String), + 'vercel.ai.response.model': 'mock-model-id', + 'vercel.ai.response.timestamp': expect.any(String), + 'vercel.ai.settings.maxRetries': 2, + 'vercel.ai.streaming': false, + 'gen_ai.request.model': 'mock-model-id', + 'gen_ai.response.finish_reasons': ['tool-calls'], + 'gen_ai.response.id': expect.any(String), + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.system': 'mock-provider', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateText.doGenerate', + 'sentry.op': 'gen_ai.generate_text', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + expect.objectContaining({ + data: { + 'vercel.ai.operationId': 'ai.toolCall', + 'gen_ai.tool.call.id': 'call-1', + 'gen_ai.tool.name': 'getWeather', + 'gen_ai.tool.type': 'function', + 'operation.name': 'ai.toolCall', + 'sentry.op': 'gen_ai.execute_tool', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'execute_tool getWeather', + op: 'gen_ai.execute_tool', + origin: 'auto.vercelai.otel', + status: 'unknown_error', + }), + ]), + + tags: { + 'test-tag': 'test-value', + }, + }; + + let traceId: string = 'unset-trace-id'; + let spanId: string = 'unset-span-id'; + + const expectedError = { + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + exception: { + values: expect.arrayContaining([ + expect.objectContaining({ + type: 'AI_ToolExecutionError', + value: 'Error executing tool getWeather: Error in tool', + }), + ]), + }, + tags: { + 'test-tag': 'test-value', + }, + }; + + await createRunner() + .expect({ + transaction: transaction => { + expect(transaction).toMatchObject(expectedTransaction); + traceId = transaction.contexts!.trace!.trace_id; + spanId = transaction.contexts!.trace!.span_id; + }, + }) + .expect({ + event: event => { + expect(event).toMatchObject(expectedError); + expect(event.contexts!.trace!.trace_id).toBe(traceId); + expect(event.contexts!.trace!.span_id).toBe(spanId); + }, + }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-error-in-tool-express.mjs', 'instrument.mjs', (createRunner, test) => { + test('captures error in tool in express server', async () => { + const expectedTransaction = { + transaction: 'GET /test/error-in-tool', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'vercel.ai.model.id': 'mock-model-id', + 'vercel.ai.model.provider': 'mock-provider', + 'vercel.ai.operationId': 'ai.generateText', + 'vercel.ai.pipeline.name': 'generateText', + 'vercel.ai.settings.maxRetries': 2, + 'vercel.ai.settings.maxSteps': 1, + 'vercel.ai.streaming': false, + 'gen_ai.response.model': 'mock-model-id', + 'operation.name': 'ai.generateText', + 'sentry.op': 'gen_ai.invoke_agent', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generateText', + op: 'gen_ai.invoke_agent', + origin: 'auto.vercelai.otel', + status: 'unknown_error', + }), + expect.objectContaining({ + data: { + 'vercel.ai.model.id': 'mock-model-id', + 'vercel.ai.model.provider': 'mock-provider', + 'vercel.ai.operationId': 'ai.generateText.doGenerate', + 'vercel.ai.pipeline.name': 'generateText.doGenerate', + 'vercel.ai.response.finishReason': 'tool-calls', + 'vercel.ai.response.id': expect.any(String), + 'vercel.ai.response.model': 'mock-model-id', + 'vercel.ai.response.timestamp': expect.any(String), + 'vercel.ai.settings.maxRetries': 2, + 'vercel.ai.streaming': false, + 'gen_ai.request.model': 'mock-model-id', + 'gen_ai.response.finish_reasons': ['tool-calls'], + 'gen_ai.response.id': expect.any(String), + 'gen_ai.response.model': 'mock-model-id', + 'gen_ai.system': 'mock-provider', + 'gen_ai.usage.input_tokens': 15, + 'gen_ai.usage.output_tokens': 25, + 'gen_ai.usage.total_tokens': 40, + 'operation.name': 'ai.generateText.doGenerate', + 'sentry.op': 'gen_ai.generate_text', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'generate_text mock-model-id', + op: 'gen_ai.generate_text', + origin: 'auto.vercelai.otel', + status: 'ok', + }), + expect.objectContaining({ + data: { + 'vercel.ai.operationId': 'ai.toolCall', + 'gen_ai.tool.call.id': 'call-1', + 'gen_ai.tool.name': 'getWeather', + 'gen_ai.tool.type': 'function', + 'operation.name': 'ai.toolCall', + 'sentry.op': 'gen_ai.execute_tool', + 'sentry.origin': 'auto.vercelai.otel', + }, + description: 'execute_tool getWeather', + op: 'gen_ai.execute_tool', + origin: 'auto.vercelai.otel', + status: 'unknown_error', + }), + ]), + + tags: { + 'test-tag': 'test-value', + }, + }; + + const expectedError = { + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + exception: { + values: expect.arrayContaining([ + expect.objectContaining({ + type: 'AI_ToolExecutionError', + value: 'Error executing tool getWeather: Error in tool', + }), + ]), + }, + tags: { + 'test-tag': 'test-value', + }, + }; + + let transactionEvent: Event | undefined; + let errorEvent: Event | undefined; + + const runner = await createRunner() + .expect({ + transaction: transaction => { + transactionEvent = transaction; + }, + }) + .expect({ + event: event => { + errorEvent = event; + }, + }) + .start(); + + await runner.makeRequest('get', '/test/error-in-tool', { expectError: true }); + await runner.completed(); + + expect(transactionEvent).toBeDefined(); + expect(errorEvent).toBeDefined(); + + expect(transactionEvent).toMatchObject(expectedTransaction); + + expect(errorEvent).toMatchObject(expectedError); + expect(errorEvent!.contexts!.trace!.trace_id).toBe(transactionEvent!.contexts!.trace!.trace_id); + expect(errorEvent!.contexts!.trace!.span_id).toBe(transactionEvent!.contexts!.trace!.span_id); + }); + }); }); diff --git a/packages/node-core/src/integrations/onunhandledrejection.ts b/packages/node-core/src/integrations/onunhandledrejection.ts index 8b41da189a0f..a11d5c3cf7b0 100644 --- a/packages/node-core/src/integrations/onunhandledrejection.ts +++ b/packages/node-core/src/integrations/onunhandledrejection.ts @@ -1,5 +1,5 @@ -import type { Client, IntegrationFn, SeverityLevel } from '@sentry/core'; -import { captureException, consoleSandbox, defineIntegration, getClient } from '@sentry/core'; +import type { Client, IntegrationFn, SeverityLevel, Span } from '@sentry/core'; +import { captureException, consoleSandbox, defineIntegration, getClient, withActiveSpan } from '@sentry/core'; import { logAndExitProcess } from '../utils/errorhandling'; type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; @@ -51,16 +51,27 @@ export function makeUnhandledPromiseHandler( const level: SeverityLevel = options.mode === 'strict' ? 'fatal' : 'error'; - captureException(reason, { - originalException: promise, - captureContext: { - extra: { unhandledPromiseRejection: true }, - level, - }, - mechanism: { - handled: false, - type: 'onunhandledrejection', - }, + // this can be set in places where we cannot reliably get access to the active span/error + // when the error bubbles up to this handler, we can use this to set the active span + const activeSpanForError = + reason && typeof reason === 'object' ? (reason as { _sentry_active_span?: Span })._sentry_active_span : undefined; + + const activeSpanWrapper = activeSpanForError + ? (fn: () => void) => withActiveSpan(activeSpanForError, fn) + : (fn: () => void) => fn(); + + activeSpanWrapper(() => { + captureException(reason, { + originalException: promise, + captureContext: { + extra: { unhandledPromiseRejection: true }, + level, + }, + mechanism: { + handled: false, + type: 'onunhandledrejection', + }, + }); }); handleRejection(reason, options.mode); diff --git a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts index 4b823670793a..22ec18a682f0 100644 --- a/packages/node/src/integrations/tracing/vercelai/instrumentation.ts +++ b/packages/node/src/integrations/tracing/vercelai/instrumentation.ts @@ -1,6 +1,12 @@ import type { InstrumentationConfig, InstrumentationModuleDefinition } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import { getCurrentScope, SDK_VERSION } from '@sentry/core'; +import { + addNonEnumerableProperty, + getActiveSpan, + getCurrentScope, + handleCallbackErrors, + SDK_VERSION, +} from '@sentry/core'; import { INTEGRATION_NAME } from './constants'; import type { TelemetrySettings, VercelAiIntegration } from './types'; @@ -132,8 +138,21 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase { recordOutputs, }; - // @ts-expect-error we know that the method exists - return originalMethod.apply(this, args); + return handleCallbackErrors( + () => { + // @ts-expect-error we know that the method exists + return originalMethod.apply(this, args); + }, + error => { + // This error bubbles up to unhandledrejection handler (if not handled before), + // where we do not know the active span anymore + // So to circumvent this, we set the active span on the error object + // which is picked up by the unhandledrejection handler + if (error && typeof error === 'object') { + addNonEnumerableProperty(error, '_sentry_active_span', getActiveSpan()); + } + }, + ); }; } From 473a4feaab2562f4ccc9ef34fbb5e231fe86a611 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 24 Jul 2025 13:40:14 +0200 Subject: [PATCH 11/14] meta(changelog): Update changelog for 9.41.0 (#17149) Co-authored-by: Charly Gomez --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ea2ac0304c..15f07ab03ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,48 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.41.0 + +### Important Changes + +- **feat(v9/core): Deprecate experimental `enableLogs` and `beforeSendLog` option ([#17092](https://github.com/getsentry/sentry-javascript/pull/17092))** + +Sentry now has support for [structured logging](https://docs.sentry.io/product/explore/logs/getting-started/). Previously to enable structured logging, you had to use the `_experiments.enableLogs` and `_experiments.beforeSendLog` options. These options have been deprecated in favor of the top-level `enableLogs` and `beforeSendLog` options. + +```js +// before +Sentry.init({ + _experiments: { + enableLogs: true, + beforeSendLog: log => { + return log; + }, + }, +}); + +// after +Sentry.init({ + enableLogs: true, + beforeSendLog: log => { + return log; + }, +}); +``` + +- **feat(astro): Implement parameterized routes** + - feat(v9/astro): Parametrize dynamic server routes ([#17141](https://github.com/getsentry/sentry-javascript/pull/17141)) + - feat(v9/astro): Parametrize routes on client-side ([#17143](https://github.com/getsentry/sentry-javascript/pull/17143)) + +Server-side and client-side parameterized routes are now supported in the Astro SDK. No configuration changes are required. + +### Other Changes + +- feat(v9/node): Add shouldHandleError option to fastifyIntegration ([#17123](https://github.com/getsentry/sentry-javascript/pull/17123)) +- fix(v9/cloudflare) Allow non UUID workflow instance IDs ([#17135](https://github.com/getsentry/sentry-javascript/pull/17135)) +- fix(v9/node): Ensure tool errors for `vercelAiIntegration` have correct trace ([#17142](https://github.com/getsentry/sentry-javascript/pull/17142)) +- fix(v9/remix): Ensure source maps upload fails silently if Sentry CLI fails ([#17095](https://github.com/getsentry/sentry-javascript/pull/17095)) +- fix(v9/svelte): Do not insert preprocess code in script module in Svelte 5 ([#17124](https://github.com/getsentry/sentry-javascript/pull/17124)) + Work in this release was contributed by @richardjelinek-fastest. Thank you for your contribution! ## 9.40.0 From 596ab227d897b1a3e46f4a893d61928954a03ee6 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 24 Jul 2025 11:43:20 +0000 Subject: [PATCH 12/14] release: 9.41.0 --- .../browser-integration-tests/package.json | 4 ++-- .../bundle-analyzer-scenarios/package.json | 2 +- dev-packages/clear-cache-gh-action/package.json | 2 +- dev-packages/e2e-tests/package.json | 2 +- .../external-contributor-gh-action/package.json | 2 +- .../node-core-integration-tests/package.json | 6 +++--- dev-packages/node-integration-tests/package.json | 8 ++++---- dev-packages/opentelemetry-v2-tests/package.json | 2 +- dev-packages/rollup-utils/package.json | 2 +- dev-packages/size-limit-gh-action/package.json | 2 +- dev-packages/test-utils/package.json | 4 ++-- lerna.json | 2 +- packages/angular/package.json | 6 +++--- packages/astro/package.json | 8 ++++---- packages/aws-serverless/package.json | 6 +++--- packages/browser-utils/package.json | 4 ++-- packages/browser/package.json | 14 +++++++------- packages/bun/package.json | 6 +++--- packages/cloudflare/package.json | 4 ++-- packages/core/package.json | 2 +- packages/deno/package.json | 4 ++-- packages/ember/package.json | 6 +++--- packages/eslint-config-sdk/package.json | 6 +++--- packages/eslint-plugin-sdk/package.json | 2 +- packages/feedback/package.json | 4 ++-- packages/gatsby/package.json | 6 +++--- packages/google-cloud-serverless/package.json | 6 +++--- packages/integration-shims/package.json | 4 ++-- packages/nestjs/package.json | 6 +++--- packages/nextjs/package.json | 14 +++++++------- packages/node-core/package.json | 6 +++--- packages/node-native/package.json | 6 +++--- packages/node/package.json | 8 ++++---- packages/nuxt/package.json | 12 ++++++------ packages/opentelemetry/package.json | 4 ++-- packages/pino-transport/package.json | 6 +++--- packages/profiling-node/package.json | 6 +++--- packages/react-router/package.json | 10 +++++----- packages/react/package.json | 6 +++--- packages/remix/package.json | 8 ++++---- packages/replay-canvas/package.json | 6 +++--- packages/replay-internal/package.json | 8 ++++---- packages/replay-worker/package.json | 2 +- packages/solid/package.json | 6 +++--- packages/solidstart/package.json | 8 ++++---- packages/svelte/package.json | 6 +++--- packages/sveltekit/package.json | 10 +++++----- packages/tanstackstart-react/package.json | 10 +++++----- packages/tanstackstart/package.json | 2 +- packages/types/package.json | 4 ++-- packages/typescript/package.json | 2 +- packages/vercel-edge/package.json | 6 +++--- packages/vue/package.json | 6 +++--- packages/wasm/package.json | 6 +++--- 54 files changed, 150 insertions(+), 150 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 35bdaec48fe3..dfae48b66099 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "9.40.0", + "version": "9.41.0", "main": "index.js", "license": "MIT", "engines": { @@ -43,7 +43,7 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.53.2", "@sentry-internal/rrweb": "2.34.0", - "@sentry/browser": "9.40.0", + "@sentry/browser": "9.41.0", "@supabase/supabase-js": "2.49.3", "axios": "1.8.2", "babel-loader": "^8.2.2", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index c227004313c9..cb54adbcef1b 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "9.40.0", + "version": "9.41.0", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index 4d868efe0480..5162e71193e3 100644 --- a/dev-packages/clear-cache-gh-action/package.json +++ b/dev-packages/clear-cache-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/clear-cache-gh-action", "description": "An internal Github Action to clear GitHub caches.", - "version": "9.40.0", + "version": "9.41.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 5ed7643090c8..c99d11aaf4fd 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "9.40.0", + "version": "9.41.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index b6753fb11284..a445231069e2 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "9.40.0", + "version": "9.41.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json index 1aa77efcf44c..aa9d28befca7 100644 --- a/dev-packages/node-core-integration-tests/package.json +++ b/dev-packages/node-core-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-core-integration-tests", - "version": "9.40.0", + "version": "9.41.0", "license": "MIT", "engines": { "node": ">=18" @@ -34,8 +34,8 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/core": "9.40.0", - "@sentry/node-core": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node-core": "9.41.0", "body-parser": "^1.20.3", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index bac4cce0e1b9..4901b1fc2ba9 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "9.40.0", + "version": "9.41.0", "license": "MIT", "engines": { "node": ">=18" @@ -30,9 +30,9 @@ "@nestjs/common": "11.1.3", "@nestjs/core": "11.1.3", "@nestjs/platform-express": "11.1.3", - "@sentry/aws-serverless": "9.40.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", + "@sentry/aws-serverless": "9.41.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 5d091acc5674..39ee571b89be 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/opentelemetry-v2-tests", - "version": "9.40.0", + "version": "9.41.0", "private": true, "description": "Tests for @sentry/opentelemetry with OpenTelemetry v2", "engines": { diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index 0928b1120f3d..6328a5d9af7a 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "9.40.0", + "version": "9.41.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index 359e4f85b51e..fe4b85bd657e 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "9.40.0", + "version": "9.41.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 30a70a3a837c..3c1a1628960c 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "9.40.0", + "version": "9.41.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,7 +45,7 @@ }, "devDependencies": { "@playwright/test": "~1.53.2", - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "volta": { "extends": "../../package.json" diff --git a/lerna.json b/lerna.json index 24a338274249..b9b9f18ca912 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "9.40.0", + "version": "9.41.0", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index 6b83cc4f42c2..7252a40077c8 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0", + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 97d212bd5bc5..93931a444e69 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,9 +56,9 @@ "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 4e04a83226ee..f42ddf6eba9c 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/aws-serverless", @@ -68,8 +68,8 @@ "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-aws-lambda": "0.50.3", "@opentelemetry/instrumentation-aws-sdk": "0.49.1", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index e0adefb02d6f..d492f149831a 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "9.40.0", + "version": "9.41.0", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index 3b20aee100a4..72cf0ef184a7 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,14 +39,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "9.40.0", - "@sentry-internal/feedback": "9.40.0", - "@sentry-internal/replay": "9.40.0", - "@sentry-internal/replay-canvas": "9.40.0", - "@sentry/core": "9.40.0" + "@sentry-internal/browser-utils": "9.41.0", + "@sentry-internal/feedback": "9.41.0", + "@sentry-internal/replay": "9.41.0", + "@sentry-internal/replay-canvas": "9.41.0", + "@sentry/core": "9.41.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "9.40.0", + "@sentry-internal/integration-shims": "9.41.0", "fake-indexeddb": "^4.0.1" }, "scripts": { diff --git a/packages/bun/package.json b/packages/bun/package.json index 7672af5c1069..6ef651a1bfa1 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0" + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 9f9a81c63592..b2fbf5e72603 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -50,7 +50,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/core/package.json b/packages/core/package.json index 0250ac4be07e..fa58d59a3903 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "9.40.0", + "version": "9.41.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/deno/package.json b/packages/deno/package.json index ba8d89664868..4a5f23a4e206 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -24,7 +24,7 @@ "/build" ], "dependencies": { - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "scripts": { "deno-types": "node ./scripts/download-deno-types.mjs", diff --git a/packages/ember/package.json b/packages/ember/package.json index e3b82471b23b..a29de241d463 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.27.7", "@embroider/macros": "^1.16.0", - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0", + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 5805c86000fd..a909d035ce4a 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "9.40.0", - "@sentry-internal/typescript": "9.40.0", + "@sentry-internal/eslint-plugin-sdk": "9.41.0", + "@sentry-internal/typescript": "9.41.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index a3ce098b410c..fd03e12da690 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 42b7a31bd69d..33fcf3589fdc 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "9.40.0", + "version": "9.41.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 564e96747e0d..ce0e45a4a6b8 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0", - "@sentry/react": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/react": "9.41.0", "@sentry/webpack-plugin": "^3.5.0" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 9adce459a322..18bfd3b30d0e 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless", @@ -48,8 +48,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 5e82bd652f5c..5f3cbb2ca9ce 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "9.40.0", + "version": "9.41.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -56,7 +56,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "engines": { "node": ">=18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 4a2d3f32c3fe..b8da707c2b3f 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -49,8 +49,8 @@ "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/instrumentation-nestjs-core": "0.44.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0" + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0" }, "devDependencies": { "@nestjs/common": "^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index aae8ce697045..06a414c58397 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -79,12 +79,12 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "9.40.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", - "@sentry/opentelemetry": "9.40.0", - "@sentry/react": "9.40.0", - "@sentry/vercel-edge": "9.40.0", + "@sentry-internal/browser-utils": "9.41.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", + "@sentry/opentelemetry": "9.41.0", + "@sentry/react": "9.41.0", + "@sentry/vercel-edge": "9.41.0", "@sentry/webpack-plugin": "^3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/node-core/package.json b/packages/node-core/package.json index 1962ff71925a..9bfe645e29f1 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-core", - "version": "9.40.0", + "version": "9.41.0", "description": "Sentry Node-Core SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-core", @@ -66,8 +66,8 @@ "@opentelemetry/semantic-conventions": "^1.34.0" }, "dependencies": { - "@sentry/core": "9.40.0", - "@sentry/opentelemetry": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/opentelemetry": "9.41.0", "import-in-the-middle": "^1.14.2" }, "devDependencies": { diff --git a/packages/node-native/package.json b/packages/node-native/package.json index 3fbb587e80b0..62d2f9e53c0e 100644 --- a/packages/node-native/package.json +++ b/packages/node-native/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-native", - "version": "9.40.0", + "version": "9.41.0", "description": "Native Tools for the Official Sentry Node.js SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-native", @@ -64,8 +64,8 @@ }, "dependencies": { "@sentry-internal/node-native-stacktrace": "^0.2.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0" + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/node/package.json b/packages/node/package.json index 9e26d5756c65..1974c902bb5b 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "9.40.0", + "version": "9.41.0", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -95,9 +95,9 @@ "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", - "@sentry/core": "9.40.0", - "@sentry/node-core": "9.40.0", - "@sentry/opentelemetry": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node-core": "9.41.0", + "@sentry/opentelemetry": "9.41.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" }, diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 8374bde60ac8..8b7845ab0437 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -47,13 +47,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "9.40.0", - "@sentry/cloudflare": "9.40.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", + "@sentry/browser": "9.41.0", + "@sentry/cloudflare": "9.41.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", "@sentry/rollup-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0", - "@sentry/vue": "9.40.0" + "@sentry/vue": "9.41.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 6ff4e1de048e..fee6b3166f85 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/pino-transport/package.json b/packages/pino-transport/package.json index a40d881234b2..a9cb8d6674bb 100644 --- a/packages/pino-transport/package.json +++ b/packages/pino-transport/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/pino-transport", - "version": "9.40.0", + "version": "9.41.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", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", "pino-abstract-transport": "^2.0.0" }, "peerDependencies": { diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index d690e8484210..7fe98645d455 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", @@ -63,8 +63,8 @@ }, "dependencies": { "@sentry-internal/node-cpu-profiler": "^2.2.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0" + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 965392e80c10..3e8f427cc7fc 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react-router", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for React Router (Framework)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router", @@ -38,11 +38,11 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/browser": "9.40.0", + "@sentry/browser": "9.41.0", "@sentry/cli": "^2.46.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", - "@sentry/react": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", + "@sentry/react": "9.41.0", "@sentry/vite-plugin": "^3.5.0", "glob": "11.0.1" }, diff --git a/packages/react/package.json b/packages/react/package.json index edbce33b1d48..764dda83402f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0", + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index 47a6ed80f568..70240b095a3c 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -69,9 +69,9 @@ "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.50.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", - "@sentry/react": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", + "@sentry/react": "9.41.0", "glob": "^10.3.4", "yargs": "^17.6.0" }, diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index ea32fd9f4e86..6647a4c6e187 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "9.40.0", + "version": "9.41.0", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -69,8 +69,8 @@ "@sentry-internal/rrweb": "2.35.0" }, "dependencies": { - "@sentry-internal/replay": "9.40.0", - "@sentry/core": "9.40.0" + "@sentry-internal/replay": "9.41.0", + "@sentry/core": "9.41.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index eb38d73cfc66..81a028aa4a5e 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "9.40.0", + "version": "9.41.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -81,7 +81,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.27.7", - "@sentry-internal/replay-worker": "9.40.0", + "@sentry-internal/replay-worker": "9.41.0", "@sentry-internal/rrweb": "2.35.0", "@sentry-internal/rrweb-snapshot": "2.35.0", "fflate": "0.8.2", @@ -90,8 +90,8 @@ "node-fetch": "^2.6.7" }, "dependencies": { - "@sentry-internal/browser-utils": "9.40.0", - "@sentry/core": "9.40.0" + "@sentry-internal/browser-utils": "9.41.0", + "@sentry/core": "9.41.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 65583f008ef0..135eaa24ebc9 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "9.40.0", + "version": "9.41.0", "description": "Worker for @sentry-internal/replay", "main": "build/esm/index.js", "module": "build/esm/index.js", diff --git a/packages/solid/package.json b/packages/solid/package.json index b32cee36f508..a14a598f97b6 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Solid", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid", @@ -44,8 +44,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0" + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0" }, "peerDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index a33c9de5cb92..b76a45074e70 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Solid Start", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart", @@ -66,9 +66,9 @@ } }, "dependencies": { - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", - "@sentry/solid": "9.40.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", + "@sentry/solid": "9.41.0", "@sentry/vite-plugin": "2.22.6" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d8d0bc3bdc6e..771958da3fff 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0", + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index eff93b7cd494..829da65ed774 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -48,10 +48,10 @@ }, "dependencies": { "@babel/parser": "7.26.9", - "@sentry/cloudflare": "9.40.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", - "@sentry/svelte": "9.40.0", + "@sentry/cloudflare": "9.41.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", + "@sentry/svelte": "9.41.0", "@sentry/vite-plugin": "^3.5.0", "magic-string": "0.30.7", "recast": "0.23.11", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 3f1fb49a61fa..fe98896dc604 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart-react", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for TanStack Start React", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart-react", @@ -52,10 +52,10 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry-internal/browser-utils": "9.40.0", - "@sentry/core": "9.40.0", - "@sentry/node": "9.40.0", - "@sentry/react": "9.40.0" + "@sentry-internal/browser-utils": "9.41.0", + "@sentry/core": "9.41.0", + "@sentry/node": "9.41.0", + "@sentry/react": "9.41.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json index e4a63ed3040b..6a4c7bc4ac49 100644 --- a/packages/tanstackstart/package.json +++ b/packages/tanstackstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart", - "version": "9.40.0", + "version": "9.41.0", "description": "Utilities for the Sentry TanStack Start SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart", diff --git a/packages/types/package.json b/packages/types/package.json index 994bb8a39568..62cdc1c8f726 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "9.40.0", + "version": "9.41.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", @@ -57,7 +57,7 @@ "yalc:publish": "yalc publish --push --sig" }, "dependencies": { - "@sentry/core": "9.40.0" + "@sentry/core": "9.41.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/typescript/package.json b/packages/typescript/package.json index cc6471d39800..2f7a7885179b 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "9.40.0", + "version": "9.41.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 63209f104add..e0bc61e20aea 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -42,8 +42,8 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/core": "9.40.0", - "@sentry/opentelemetry": "9.40.0" + "@sentry/core": "9.41.0", + "@sentry/opentelemetry": "9.41.0" }, "devDependencies": { "@edge-runtime/types": "3.0.1", diff --git a/packages/vue/package.json b/packages/vue/package.json index ec04a00b1b20..53921706a6fe 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "9.40.0", + "version": "9.41.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0" + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0" }, "peerDependencies": { "pinia": "2.x || 3.x", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 319e62de4cf8..0eb30dc3f8e1 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "9.40.0", + "version": "9.41.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.40.0", - "@sentry/core": "9.40.0" + "@sentry/browser": "9.41.0", + "@sentry/core": "9.41.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", From 3327c04626fbf11d03f050c74cda7f2204639ea3 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Thu, 24 Jul 2025 14:05:37 +0200 Subject: [PATCH 13/14] feat(v9/aws): Detect SDK source for AWS Lambda layer (#17150) backport of #17128 --- .../aws-lambda-layer-cjs/tests/basic.test.ts | 5 +++++ .../test-applications/aws-serverless-esm/tests/basic.test.ts | 5 +++++ dev-packages/rollup-utils/bundleHelpers.mjs | 1 + packages/aws-serverless/src/sdk.ts | 3 ++- packages/core/src/utils/env.ts | 2 +- 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts index 3393b2a559dd..9f88e3961a5a 100644 --- a/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/tests/basic.test.ts @@ -69,4 +69,9 @@ test('Lambda layer SDK bundle sends events', async ({ request }) => { op: 'test', }), ); + + // shows that the SDK source is correctly detected + expect(transactionEvent.sdk?.packages).toContainEqual( + expect.objectContaining({ name: 'aws-lambda-layer:@sentry/aws-serverless' }), + ); }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts index b27e16bdaa85..803484881837 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts @@ -82,4 +82,9 @@ test('AWS Serverless SDK sends events in ESM mode', async ({ request }) => { op: 'manual', }), ); + + // shows that the SDK source is correctly detected + expect(transactionEvent.sdk?.packages).toContainEqual( + expect.objectContaining({ name: 'npm:@sentry/aws-serverless' }), + ); }); diff --git a/dev-packages/rollup-utils/bundleHelpers.mjs b/dev-packages/rollup-utils/bundleHelpers.mjs index f80b0b7c2e50..18088f59d3a4 100644 --- a/dev-packages/rollup-utils/bundleHelpers.mjs +++ b/dev-packages/rollup-utils/bundleHelpers.mjs @@ -98,6 +98,7 @@ export function makeBaseBundleConfig(options) { plugins: [ jsonPlugin, commonJSPlugin, + makeSetSDKSourcePlugin('aws-lambda-layer'), // Temporary fix for the lambda layer SDK bundle. // This is necessary to apply to our lambda layer bundle because calling `new ImportInTheMiddle()` will throw an // that `ImportInTheMiddle` is not a constructor. Instead we modify the code to call `new ImportInTheMiddle.default()` diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index dafaf780ed99..f64b62b9a373 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -2,6 +2,7 @@ import type { Integration, Options, Scope, Span } from '@sentry/core'; import { applySdkMetadata, debug, + getSDKSource, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; @@ -81,7 +82,7 @@ export function init(options: NodeOptions = {}): NodeClient | undefined { ...options, }; - applySdkMetadata(opts, 'aws-serverless'); + applySdkMetadata(opts, 'aws-serverless', ['aws-serverless'], getSDKSource()); return initWithoutDefaultIntegrations(opts); } diff --git a/packages/core/src/utils/env.ts b/packages/core/src/utils/env.ts index 6f8c7ca8e946..86872017707a 100644 --- a/packages/core/src/utils/env.ts +++ b/packages/core/src/utils/env.ts @@ -15,7 +15,7 @@ declare const __SENTRY_BROWSER_BUNDLE__: boolean | undefined; -export type SdkSource = 'npm' | 'cdn' | 'loader'; +export type SdkSource = 'npm' | 'cdn' | 'loader' | 'aws-lambda-layer'; /** * Figures out if we're building a browser bundle. From 7199ab8b29b4923e625ef13898257310b7eda565 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Thu, 24 Jul 2025 16:00:19 +0200 Subject: [PATCH 14/14] fix(core): Update ai.response.object to gen_ai.response.object --- packages/core/src/utils/vercel-ai.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai.ts index c5491376d7c4..9c20d49ea157 100644 --- a/packages/core/src/utils/vercel-ai.ts +++ b/packages/core/src/utils/vercel-ai.ts @@ -10,6 +10,7 @@ import { AI_PROMPT_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE, AI_PROMPT_TOOLS_ATTRIBUTE, + AI_RESPONSE_OBJECT_ATTRIBUTE, AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE, AI_RESPONSE_TEXT_ATTRIBUTE, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, @@ -93,6 +94,7 @@ function processEndedVercelAiSpan(span: SpanJSON): void { renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages'); renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); + renameAttributeKey(attributes, AI_RESPONSE_OBJECT_ATTRIBUTE, 'gen_ai.response.object'); renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools'); renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input');