diff --git a/MIGRATION.md b/MIGRATION.md index 05c9062c4ce4..bceee9a93676 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -208,11 +208,16 @@ Sentry.init({ }); ``` -### `attachStacktrace` defaults to `true` for `captureMessage` +### `attachStacktrace` defaults to `true` Affected SDKs: All SDKs. -`captureMessage` now attaches a stack trace by default. Pass `attachStacktrace: false` in `Sentry.init` if you do not want stack traces attached to messages. Note that grouping in Sentry differs for events with and without stack traces, so you may see new issue groups after upgrading. +`attachStacktrace` now defaults to `true`. Events captured with `Sentry.captureMessage`, and non-`Error` values passed to `Sentry.captureException`, now attach a synthetic stack trace pointing to the call site. Pass `attachStacktrace: false` in `Sentry.init` to restore the previous behavior. + +Two consequences to be aware of when upgrading: + +- **Issue grouping:** Grouping in Sentry differs for events with and without stack traces, so you may see new issue groups after upgrading. +- **Release health:** Events with a stack trace are counted as errors, so a `captureMessage` call (including messages emitted by `captureConsoleIntegration`) now marks the current session as _errored_. This affects errored-session counts but does **not** mark sessions as crashed, so crash-free session rate is unaffected. If you use `captureMessage` for purely informational output, consider using Sentry Logs instead, which is better suited and does not affect release health. ### `tracePropagationTargets` matching is now case-insensitive diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js index 1d611ebed805..d265bdde9ed3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/init.js @@ -6,4 +6,5 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [captureConsoleIntegration()], + attachStacktrace: false, }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message/test.ts index 38648ff7982f..474eb9e584da 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -10,4 +10,15 @@ sentryTest('should capture a simple message string', async ({ getLocalTestUrl, p expect(eventData.message).toBe('foo'); expect(eventData.level).toBe('info'); + expect(eventData.exception?.values?.[0]).toEqual({ + mechanism: { + handled: true, + type: 'generic', + synthetic: true, + }, + stacktrace: { + frames: expect.arrayContaining([expect.any(Object), expect.any(Object)]), + }, + value: 'foo', + }); }); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/init.js new file mode 100644 index 000000000000..609b2b8c3b5f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + attachStacktrace: false, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/subject.js new file mode 100644 index 000000000000..cf462c59a2fb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/subject.js @@ -0,0 +1 @@ +Sentry.captureMessage('foo'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts new file mode 100644 index 000000000000..ae9db5f3064d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts @@ -0,0 +1,14 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('does not capture a stack trace if `attachStackTrace` is `false`', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.message).toBe('foo'); + expect(eventData.level).toBe('info'); + expect(eventData.exception).toBeUndefined(); +}); diff --git a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/index.ts b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/index.ts index d8da65ad2e1a..367a12f4e37f 100644 --- a/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/integrations/http-server/index.ts @@ -7,6 +7,7 @@ interface Env { export default Sentry.withSentry( (env: Env) => ({ dsn: env.SENTRY_DSN, + attachStacktrace: false, }), { async fetch(request, _env, _ctx) { diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts index e32081747f28..96a041e7bfe1 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -1,4 +1,4 @@ -import { afterAll, test } from 'vitest'; +import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { @@ -11,6 +11,15 @@ test('should capture a simple message string', async () => { event: { message: 'Message', level: 'info', + exception: { + values: [ + { + mechanism: { synthetic: true, type: 'generic', handled: true }, + value: 'Message', + stacktrace: { frames: expect.any(Array) }, + }, + ], + }, }, }) .start() diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/scenario.ts new file mode 100644 index 000000000000..2dbc1c39df9e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/scenario.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + attachStacktrace: false, +}); + +Sentry.captureMessage('Message'); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts new file mode 100644 index 000000000000..25953d61b907 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts @@ -0,0 +1,19 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('does not capture a stack trace if `attachStackTrace` is `false`', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: event => { + expect(event.message).toBe('Message'); + expect(event.level).toBe('info'); + expect(event.exception).toBeUndefined(); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts index a81172775fbc..8ae145b43b71 100644 --- a/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts @@ -5,6 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + attachStacktrace: false, }); Sentry.captureMessage('debug_message', 'debug'); diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts index b01493237679..f9245b584256 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts @@ -5,6 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + attachStacktrace: false, }); const globalScope = Sentry.getGlobalScope(); diff --git a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts index 58e6b3d07560..b527d14699cc 100644 --- a/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts @@ -5,6 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + attachStacktrace: false, }); const globalScope = Sentry.getGlobalScope(); diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts index 957bb7841d89..9e617168b539 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/unset_user/scenario.ts @@ -5,6 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + attachStacktrace: false, }); Sentry.captureMessage('no_user'); diff --git a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts index a7f5df98d614..4c2eb2221bf6 100644 --- a/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/setUser/update_user/scenario.ts @@ -5,6 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + attachStacktrace: false, }); Sentry.setUser({ diff --git a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts index a0d383d70841..93f425ff6a89 100644 --- a/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts @@ -5,6 +5,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + attachStacktrace: false, }); Sentry.setUser({ id: 'qux' }); diff --git a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts index e9e39d1500af..dc3bd6df6611 100644 --- a/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts +++ b/dev-packages/node-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts @@ -6,6 +6,7 @@ Sentry.init({ release: '1.0', tracesSampleRate: 1.0, transport: loggingTransport, + attachStacktrace: false, }); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index c2316fed27c5..346db4e17e9e 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -334,8 +334,9 @@ export function eventFromUnknownInput( // - a plain Object // // So bail out and capture it as a simple message: - event = eventFromString(stackParser, exception as string, syntheticException, attachStacktrace); - addExceptionTypeValue(event, `${exception}`, undefined); + const stringifiedException = String(exception); + event = eventFromString(stackParser, stringifiedException, syntheticException, attachStacktrace); + addExceptionTypeValue(event, stringifiedException, undefined); addExceptionMechanism(event, { synthetic: true, }); diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index bdf5127243b2..13386010cfdd 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -169,6 +169,30 @@ describe('eventFromUnknownInput', () => { }); }); + it('uses the stringified value for a non-Error input when attachStacktrace is true', async () => { + const syntheticException = new Error('Test message'); + const event = await eventFromUnknownInput(defaultStackParser, new Response('test body'), syntheticException, true); + + expect(event.exception?.values?.[0]).toEqual( + expect.objectContaining({ + mechanism: { handled: true, synthetic: true, type: 'generic' }, + type: 'Error', + value: '[object Response]', + }), + ); + }); + + it('does not throw and stringifies the value for a Symbol input', async () => { + const event = await eventFromUnknownInput(defaultStackParser, Symbol('foo')); + + expect(event.exception?.values?.[0]).toEqual( + expect.objectContaining({ + type: 'Error', + value: 'Symbol(foo)', + }), + ); + }); + it('add a synthetic stack trace to DOMException with empty stack traces if attachStacktrace is true', async () => { const exception = new DOMException('The string did not match the expected pattern.', 'SyntaxError'); exception.stack = ''; diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index b662352bf3e3..78af0a11d915 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -19,6 +19,7 @@ import { captureEvent, captureException, captureMessage, + defaultStackParser, flush, getClient, getCurrentScope, @@ -245,6 +246,7 @@ describe('SentryBrowser', () => { it('should capture an message', () => new Promise(resolve => { const options = getDefaultBrowserClientOptions({ + attachStacktrace: false, beforeSend: event => { expect(event.level).toBe('info'); expect(event.message).toBe('test'); @@ -258,6 +260,23 @@ describe('SentryBrowser', () => { captureMessage('test'); })); + it('attaches a synthetic stacktrace to messages by default', () => + new Promise(resolve => { + const options = getDefaultBrowserClientOptions({ + stackParser: defaultStackParser, + beforeSend: event => { + expect(event.message).toBe('test'); + expect(event.exception?.values?.[0]?.stacktrace?.frames?.length).toBeGreaterThan(0); + expect(event.exception?.values?.[0]?.mechanism?.synthetic).toBe(true); + resolve(); + return event; + }, + dsn, + }); + setCurrentClient(new BrowserClient(options)); + captureMessage('test'); + })); + it('should capture an event', () => new Promise(resolve => { const options = getDefaultBrowserClientOptions({ diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0056484d01e4..c5c186c75794 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -234,7 +234,7 @@ export abstract class Client { * @param options Options for the client. */ protected constructor(options: O) { - this._options = options; + this._options = { attachStacktrace: true, ...options }; this._integrations = {}; this._numProcessing = 0; this._outcomes = {}; diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 3d55c5f17498..a161d8b36e10 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -182,12 +182,14 @@ export interface ClientOptions { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, test: true }); const client = new TestClient(options); - expect(client.getOptions()).toEqual(options); + expect(client.getOptions()).toEqual({ attachStacktrace: true, ...options }); }); }); diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 7da66392eb30..416bde219f05 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -48,7 +48,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "?", in_app: true, - lineno: 42, + lineno: 43, post_context: [ "", " await delay(200);", @@ -74,7 +74,7 @@ snapshot[`captureException 1`] = ` filename: "app:///test/mod.test.ts", function: "something", in_app: true, - lineno: 39, + lineno: 40, post_context: [ " }", "", diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index ecc3a6d4fe9e..1a2e376dd433 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -12,6 +12,7 @@ function getTestClient(callback: (event?: Event) => void): DenoClient { debug: true, integrations: getDefaultIntegrations({}), stackParser: createStackParser(nodeStackLineParser()), + attachStacktrace: false, transport: makeTestTransport(envelope => { callback(getNormalizedEvent(envelope)); }), diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 8dcdf33d4067..c581f07b5c6a 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -29,6 +29,7 @@ describe('NodeClient', () => { const client = new NodeClient(options); expect(client.getOptions()).toEqual({ + attachStacktrace: true, dsn: expect.any(String), integrations: [], transport: options.transport,