Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ window.Sentry = Sentry;
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [captureConsoleIntegration()],
attachStacktrace: false,

@nicohrubec nicohrubec Jul 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opted out some tests that are not directly about the stacktrace before to not have to adjust the assertions, but can also switch this if we prefer to test the default case everywhere

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you found a good balance of opting some tests out of the change while adjusting some others. No objections!

});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sentry.captureMessage('foo');
Original file line number Diff line number Diff line change
@@ -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<Event>(page, url);

expect(eventData.message).toBe('foo');
expect(eventData.level).toBe('info');
expect(eventData.exception).toBeUndefined();
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface Env {
export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
attachStacktrace: false,
}),
{
async fetch(request, _env, _ctx) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterAll, test } from 'vitest';
import { afterAll, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';

afterAll(() => {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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();
});
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
transport: loggingTransport,
attachStacktrace: false,
});

Sentry.setUser({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/browser/src/eventbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
24 changes: 24 additions & 0 deletions packages/browser/test/eventbuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
19 changes: 19 additions & 0 deletions packages/browser/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
captureEvent,
captureException,
captureMessage,
defaultStackParser,
flush,
getClient,
getCurrentScope,
Expand Down Expand Up @@ -245,6 +246,7 @@ describe('SentryBrowser', () => {
it('should capture an message', () =>
new Promise<void>(resolve => {
const options = getDefaultBrowserClientOptions({
attachStacktrace: false,
beforeSend: event => {
expect(event.level).toBe('info');
expect(event.message).toBe('test');
Expand All @@ -258,6 +260,23 @@ describe('SentryBrowser', () => {
captureMessage('test');
}));

it('attaches a synthetic stacktrace to messages by default', () =>
Comment thread
cursor[bot] marked this conversation as resolved.
new Promise<void>(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<void>(resolve => {
const options = getDefaultBrowserClientOptions({
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
* @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 = {};
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,14 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
enabled?: boolean;

/**
* When enabled, stack traces are automatically attached to all events captured with `Sentry.captureMessage`.
* Stack traces are automatically attached to events that don't otherwise have one. This applies
* to events captured with `Sentry.captureMessage` and to non-Error values passed to
* `Sentry.captureException`. Set this to `false` to disable attaching these stack traces.
*
* Grouping in Sentry is different for events with stack traces and without. As a result, you will get
* new groups as you enable or disable this flag for certain events.
*
* @default false
* @default true
*/
attachStacktrace?: boolean;

Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/deno/test/__snapshots__/mod.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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);",
Expand All @@ -74,7 +74,7 @@ snapshot[`captureException 1`] = `
filename: "app:///test/mod.test.ts",
function: "something",
in_app: true,
lineno: 39,
lineno: 40,
post_context: [
" }",
"",
Expand Down
1 change: 1 addition & 0 deletions packages/deno/test/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}),
Expand Down
1 change: 1 addition & 0 deletions packages/node/test/sdk/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading