From f46f26a22b9ce3c45840e8a6ff6a3e1deb4fafe1 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:52:13 +0200 Subject: [PATCH] fix(client): Keep captureMessage stack trace in exception (#3988) Co-authored-by: LucasZF --- CHANGELOG.md | 7 +++++++ src/js/client.ts | 34 +++++++++++++++++++--------------- src/js/options.ts | 8 ++++++++ test/client.test.ts | 33 +++++++++++++++++++++++++++------ 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b3ffcfb..c0a5b995d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Fixes + +- `Sentry.captureMessage` stack trace is in `event.exception` (moved from `event.threads`) ([#3635](https://github.com/getsentry/sentry-react-native/pull/3635), [#3988](https://github.com/getsentry/sentry-react-native/pull/3988)) + - To revert to the old behavior (causing the stack to be unsymbolicated) use `useThreadsForMessageStack` option + ## 5.27.0 ### Fixes diff --git a/src/js/client.ts b/src/js/client.ts index cc45b15e8..e22eceebd 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -59,22 +59,26 @@ export class ReactNativeClient extends BaseClient { * @inheritDoc */ public eventFromMessage(message: string, level?: SeverityLevel, hint?: EventHint): PromiseLike { - return eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace).then( - (event: Event) => { - // TMP! Remove this function once JS SDK uses threads for messages - if (!event.exception?.values || event.exception.values.length <= 0) { + if (this._options.useThreadsForMessageStack) { + return eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace).then( + (event: Event) => { + // TMP! Remove this function once JS SDK uses threads for messages + if (!event.exception?.values || event.exception.values.length <= 0) { + return event; + } + const values = event.exception.values.map( + (exception: Exception): Thread => ({ + stacktrace: exception.stacktrace, + }), + ); + (event as { threads?: { values: Thread[] } }).threads = { values }; + delete event.exception; return event; - } - const values = event.exception.values.map( - (exception: Exception): Thread => ({ - stacktrace: exception.stacktrace, - }), - ); - (event as { threads?: { values: Thread[] } }).threads = { values }; - delete event.exception; - return event; - }, - ); + }, + ); + } + + return eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace); } /** diff --git a/src/js/options.ts b/src/js/options.ts index 0c5a4baa4..d798b4220 100644 --- a/src/js/options.ts +++ b/src/js/options.ts @@ -213,6 +213,14 @@ export interface BaseReactNativeOptions { */ replaysOnErrorSampleRate?: number; }; + + /** + * This options changes the placement of the attached stacktrace of `captureMessage` in the event. + * + * @default false + * @deprecated This option will be removed in the next major version. Use `beforeSend` instead. + */ + useThreadsForMessageStack?: boolean; } export interface ReactNativeTransportOptions extends BrowserTransportOptions { diff --git a/test/client.test.ts b/test/client.test.ts index 2cd761f78..d6eafc156 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -349,6 +349,16 @@ describe('Tests ReactNativeClient', () => { beforeEach(() => { mockTransportSend = jest.fn(() => Promise.resolve()); + }); + + afterEach(() => { + mockTransportSend.mockClear(); + }); + + const getMessageEventFrom = (func: jest.Mock) => + func.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload]; + + test('captureMessage contains stack trace in exception', async () => { client = new ReactNativeClient({ ...DEFAULT_OPTIONS, attachStacktrace: true, @@ -359,16 +369,27 @@ describe('Tests ReactNativeClient', () => { flush: jest.fn(), }), } as ReactNativeClientOptions); - }); - afterEach(() => { - mockTransportSend.mockClear(); + const mockSyntheticExceptionFromHub = new Error(); + client.captureMessage('test message', 'error', { syntheticException: mockSyntheticExceptionFromHub }); + expect(getMessageEventFrom(mockTransportSend).exception.values.length).toBeGreaterThan(0); + expect(getMessageEventFrom(mockTransportSend).exception).toBeDefined(); + expect(getMessageEventFrom(mockTransportSend).threads).toBeUndefined(); }); - const getMessageEventFrom = (func: jest.Mock) => - func.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload]; + test('captureMessage contains stack trace in exception', async () => { + client = new ReactNativeClient({ + ...DEFAULT_OPTIONS, + attachStacktrace: true, + stackParser: defaultStackParser, + dsn: EXAMPLE_DSN, + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + useThreadsForMessageStack: true, + } as ReactNativeClientOptions); - test('captureMessage contains stack trace in threads', async () => { const mockSyntheticExceptionFromHub = new Error(); client.captureMessage('test message', 'error', { syntheticException: mockSyntheticExceptionFromHub }); expect(getMessageEventFrom(mockTransportSend).threads.values.length).toBeGreaterThan(0);