From 2b0b9053f21caaeedb5531195ee98711e3a9d6f5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 07:43:19 +0200 Subject: [PATCH 01/10] feat(core): Default attachStacktrace to true Attach synthetic stack traces by default for message/string-derived events. Explicit `attachStacktrace: false` still opts out. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../captureMessage/simple_message/test.ts | 11 ++++++++ .../captureMessage/simple_message/test.ts | 11 +++++++- packages/browser/src/eventbuilder.ts | 4 +-- packages/browser/test/eventbuilder.test.ts | 14 ++++++++++ packages/core/src/types/options.ts | 5 ++-- packages/core/src/utils/eventbuilder.ts | 2 +- .../core/test/lib/utils/eventbuilder.test.ts | 26 +++++++++++++++++++ 7 files changed, 67 insertions(+), 6 deletions(-) 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/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/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index c2316fed27c5..1b4da6812fc1 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -289,7 +289,7 @@ export function eventFromUnknownInput( event = eventFromError(stackParser, exception as Error); const firstException = event.exception?.values?.[0]; - if (attachStacktrace && syntheticException && firstException && !firstException.stacktrace) { + if (attachStacktrace !== false && syntheticException && firstException && !firstException.stacktrace) { const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { firstException.stacktrace = { frames }; @@ -351,7 +351,7 @@ function eventFromString( ): Event { const event: Event = {}; - if (attachStacktrace && syntheticException) { + if (attachStacktrace !== false && syntheticException) { const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { event.exception = { diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index bdf5127243b2..a7ddbd778907 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -326,6 +326,20 @@ describe('eventFromMessage ', () => { }); }); + it('adds a synthetic stack trace by default when attachStacktrace is not set', async () => { + const syntheticException = new Error('Test message'); + const event = await eventFromMessage(defaultStackParser, 'Test message', 'info', { syntheticException }); + expect(event.exception?.values?.[0]).toEqual( + expect.objectContaining({ + mechanism: { handled: true, synthetic: true, type: 'generic' }, + stacktrace: { + frames: expect.arrayContaining([expect.any(Object), expect.any(Object)]), + }, + value: 'Test message', + }), + ); + }); + it('creates an event with a synthetic stack trace if attachStacktrace is true', async () => { const syntheticException = new Error('Test message'); const event = await eventFromMessage(defaultStackParser, 'Test message', 'info', { syntheticException }, true); diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 3d55c5f17498..7cd1e81f1031 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -182,12 +182,13 @@ export interface ClientOptions { }); }); + it('attaches a synthetic exception by default when `attachStackTrace` is not set', () => { + const syntheticException = new Error('Test Message'); + const event = eventFromMessage(stackParser, 'Test Message', 'info', { syntheticException, event_id: '123abc' }); + + expect(event).toEqual({ + event_id: '123abc', + exception: { + values: [ + { + mechanism: { + handled: true, + synthetic: true, + type: 'generic', + }, + stacktrace: { + frames: expect.any(Array), + }, + value: 'Test Message', + }, + ], + }, + level: 'info', + message: 'Test Message', + }); + }); + it('attaches a synthetic exception if passed and `attachStackTrace` is true', () => { const syntheticException = new Error('Test Message'); const event = eventFromMessage( From e4f5787cc1360ecdf75fee9ea5f08384796084de Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 07:53:28 +0200 Subject: [PATCH 02/10] test(core): Cover attachStacktrace opt-out; clarify option docs Add browser + node integration suites asserting `attachStacktrace: false` produces no stack trace, and reword the option JSDoc to reflect that it also applies to non-Error values passed to captureException. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../simple_message_no_stacktrace/init.js | 8 ++++++++ .../simple_message_no_stacktrace/subject.js | 1 + .../simple_message_no_stacktrace/test.ts | 14 ++++++++++++++ .../simple_message_no_stacktrace/scenario.ts | 11 +++++++++++ .../simple_message_no_stacktrace/test.ts | 19 +++++++++++++++++++ packages/core/src/types/options.ts | 5 +++-- 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_no_stacktrace/test.ts 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/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/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 7cd1e81f1031..a161d8b36e10 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -182,8 +182,9 @@ export interface ClientOptions Date: Fri, 24 Jul 2026 08:25:26 +0200 Subject: [PATCH 03/10] test: Opt out of attachStacktrace in tests unrelated to stack traces With attachStacktrace now defaulting to true, message events gained a synthetic exception. This broke tests that assert message events have no exception (browser captureConsole, cloudflare http-server, deno snapshots) and node scope/DSC tests whose strict envelope ordering shifted because the synthetic stack trace triggers async ContextLines source reads. None of these tests concern stack traces, so opt them out with attachStacktrace: false rather than reworking their assertions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/integrations/captureConsole/init.js | 1 + .../suites/integrations/http-server/index.ts | 1 + .../suites/public-api/scopes/isolationScope/scenario.ts | 1 + .../suites/public-api/withScope/nested-scopes/scenario.ts | 1 + .../suites/tracing/dsc-txn-name-update/scenario-events.ts | 1 + packages/deno/test/mod.test.ts | 1 + 6 files changed, 6 insertions(+) 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/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/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/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/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)); }), From 7b4afa14a99edd02c179f87639c9ca60e0f1704f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 08:42:25 +0200 Subject: [PATCH 04/10] fix(browser): Preserve stringified value for non-Error captureException inputs With attachStacktrace defaulting to true, non-Error values passed to captureException (e.g. a Response) now hit the exception-building branch in eventFromString, which set `value` to the raw object (serialized to `{}`) instead of the readable `[object Response]` string. Pass the stringified input so the value matches the pre-existing behavior. Also bump the deno captureException snapshot line numbers shifted by the test-only opt-out. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/browser/src/eventbuilder.ts | 2 +- packages/browser/test/eventbuilder.test.ts | 13 +++++++++++++ packages/deno/test/__snapshots__/mod.test.ts.snap | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 1b4da6812fc1..84691e51880e 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -334,7 +334,7 @@ export function eventFromUnknownInput( // - a plain Object // // So bail out and capture it as a simple message: - event = eventFromString(stackParser, exception as string, syntheticException, attachStacktrace); + event = eventFromString(stackParser, `${exception}`, syntheticException, attachStacktrace); addExceptionTypeValue(event, `${exception}`, undefined); addExceptionMechanism(event, { synthetic: true, diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index a7ddbd778907..6e1030d498c6 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -169,6 +169,19 @@ 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('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/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: [ " }", "", From 9f69f07132ee4207f731c5e67845fba70f7e35fc Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 09:21:33 +0200 Subject: [PATCH 05/10] test: Opt out of attachStacktrace in remaining order-sensitive message suites An audit of all node-integration suites found four more that fire multiple captureMessage calls and assert them in strict envelope order. With the synthetic stack trace now attached by default, async ContextLines source reads race and reorder emission, flaking these tests. Opt them out with attachStacktrace: false like the others; none concern stack traces. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/public-api/captureMessage/with_level/scenario.ts | 1 + .../suites/public-api/scopes/initialScopes/scenario.ts | 1 + .../suites/public-api/setUser/unset_user/scenario.ts | 1 + .../suites/public-api/setUser/update_user/scenario.ts | 1 + 4 files changed, 4 insertions(+) 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/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({ From 5ee12711d0af589952726073bf0cdb5e2ff19809 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 11:43:12 +0200 Subject: [PATCH 06/10] refactor(core): Centralize attachStacktrace default in client constructor Set attachStacktrace to true once in the base Client constructor instead of asserting `!== false` at each stacktrace guard. The guards revert to plain truthy checks and every SDK client inherits the default via super(). This is easier to reason about and slightly smaller. Move the message default-on assertion to a client-level browser test, since the eventFromMessage helper no longer defaults on its own. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/browser/src/eventbuilder.ts | 4 +-- packages/browser/test/eventbuilder.test.ts | 14 ---------- packages/browser/test/index.test.ts | 18 +++++++++++++ packages/core/src/client.ts | 2 +- packages/core/src/utils/eventbuilder.ts | 2 +- packages/core/test/lib/client.test.ts | 2 +- .../core/test/lib/utils/eventbuilder.test.ts | 26 ------------------- 7 files changed, 23 insertions(+), 45 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 84691e51880e..b300498e522f 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -289,7 +289,7 @@ export function eventFromUnknownInput( event = eventFromError(stackParser, exception as Error); const firstException = event.exception?.values?.[0]; - if (attachStacktrace !== false && syntheticException && firstException && !firstException.stacktrace) { + if (attachStacktrace && syntheticException && firstException && !firstException.stacktrace) { const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { firstException.stacktrace = { frames }; @@ -351,7 +351,7 @@ function eventFromString( ): Event { const event: Event = {}; - if (attachStacktrace !== false && syntheticException) { + if (attachStacktrace && syntheticException) { const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { event.exception = { diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index 6e1030d498c6..787bcbf2dee4 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -339,20 +339,6 @@ describe('eventFromMessage ', () => { }); }); - it('adds a synthetic stack trace by default when attachStacktrace is not set', async () => { - const syntheticException = new Error('Test message'); - const event = await eventFromMessage(defaultStackParser, 'Test message', 'info', { syntheticException }); - expect(event.exception?.values?.[0]).toEqual( - expect.objectContaining({ - mechanism: { handled: true, synthetic: true, type: 'generic' }, - stacktrace: { - frames: expect.arrayContaining([expect.any(Object), expect.any(Object)]), - }, - value: 'Test message', - }), - ); - }); - it('creates an event with a synthetic stack trace if attachStacktrace is true', async () => { const syntheticException = new Error('Test message'); const event = await eventFromMessage(defaultStackParser, 'Test message', 'info', { syntheticException }, true); diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index b662352bf3e3..2aace5e258ee 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, @@ -258,6 +259,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/utils/eventbuilder.ts b/packages/core/src/utils/eventbuilder.ts index b1948a849772..8c1525ef7e74 100644 --- a/packages/core/src/utils/eventbuilder.ts +++ b/packages/core/src/utils/eventbuilder.ts @@ -199,7 +199,7 @@ export function eventFromMessage( level, }; - if (attachStacktrace !== false && hint?.syntheticException) { + if (attachStacktrace && hint?.syntheticException) { const frames = parseStackFrames(stackParser, hint.syntheticException); if (frames.length) { event.exception = { diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index e37c32dea309..aff5daedbaea 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -103,7 +103,7 @@ describe('Client', () => { 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/core/test/lib/utils/eventbuilder.test.ts b/packages/core/test/lib/utils/eventbuilder.test.ts index 573317956594..b882a4562b1c 100644 --- a/packages/core/test/lib/utils/eventbuilder.test.ts +++ b/packages/core/test/lib/utils/eventbuilder.test.ts @@ -167,32 +167,6 @@ describe('eventFromMessage', () => { }); }); - it('attaches a synthetic exception by default when `attachStackTrace` is not set', () => { - const syntheticException = new Error('Test Message'); - const event = eventFromMessage(stackParser, 'Test Message', 'info', { syntheticException, event_id: '123abc' }); - - expect(event).toEqual({ - event_id: '123abc', - exception: { - values: [ - { - mechanism: { - handled: true, - synthetic: true, - type: 'generic', - }, - stacktrace: { - frames: expect.any(Array), - }, - value: 'Test Message', - }, - ], - }, - level: 'info', - message: 'Test Message', - }); - }); - it('attaches a synthetic exception if passed and `attachStackTrace` is true', () => { const syntheticException = new Error('Test Message'); const event = eventFromMessage( From e3bd6db165b34fbf8010b90dce0e14532cab87cd Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 11:55:50 +0200 Subject: [PATCH 07/10] fix(browser): Use String() to stringify non-Error captureException inputs `${exception}` throws a TypeError for Symbol inputs. Use String() instead, which safely handles Symbols while producing identical output for all other values. This was a pre-existing issue on the addExceptionTypeValue line. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/browser/src/eventbuilder.ts | 5 +++-- packages/browser/test/eventbuilder.test.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index b300498e522f..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}`, 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 787bcbf2dee4..13386010cfdd 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -182,6 +182,17 @@ describe('eventFromUnknownInput', () => { ); }); + 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 = ''; From 3928105ab33cfe041a11199a81aeebcea56d78ac Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 12:04:10 +0200 Subject: [PATCH 08/10] test(browser): Make message no-exception test explicit about attachStacktrace The pre-existing "should capture an message" test asserted no exception but only passed due to the empty stackParser, reading as a contradiction with the new default-case test beside it. Set attachStacktrace: false so the assertion reflects the opt-out path explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/browser/test/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index 2aace5e258ee..78af0a11d915 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -246,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'); From 24699b5d4b98a5c20fd641fbe6a66162680f0205 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 12:10:59 +0200 Subject: [PATCH 09/10] docs: Expand attachStacktrace migration note with release-health impact Broaden the v10->v11 migration entry to cover non-Error captureException inputs and to call out that message/console events now mark sessions as errored (not crashed), plus the Logs alternative for informational messages. Co-Authored-By: Claude Opus 4.8 (1M context) --- MIGRATION.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 From ea1e77a041699e6aaaa518c5d6554cd9ceea539e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 24 Jul 2026 12:39:30 +0200 Subject: [PATCH 10/10] test(node): Expect attachStacktrace default in NodeClient metadata test The exact-match getOptions() assertion now needs the attachStacktrace: true default injected by the base Client constructor. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/node/test/sdk/client.test.ts | 1 + 1 file changed, 1 insertion(+) 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,