diff --git a/CHANGELOG.md b/CHANGELOG.md index dd9919480d..f3c3641b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Latest changes from 4.12.0 +### Breaking changes + +- Message event current stack trace moved from exception to threads ([#2694](https://github.com/getsentry/sentry-react-native/pull/2694)) + ## 4.12.0 ### Features diff --git a/src/js/client.ts b/src/js/client.ts index ebf79ed0ed..f124b7aa05 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -1,4 +1,4 @@ -import { BrowserClient, defaultStackParser, makeFetchTransport } from '@sentry/browser'; +import { eventFromException, eventFromMessage,makeFetchTransport } from '@sentry/browser'; import { FetchImpl } from '@sentry/browser/types/transports/utils'; import { BaseClient } from '@sentry/core'; import { @@ -7,8 +7,10 @@ import { Envelope, Event, EventHint, + Exception, Outcome, SeverityLevel, + Thread, Transport, UserFeedback, } from '@sentry/types'; @@ -34,26 +36,24 @@ export class ReactNativeClient extends BaseClient { private _outcomesBuffer: Outcome[]; - private readonly _browserClient: BrowserClient; - /** * Creates a new React Native SDK instance. * @param options Configuration options for this SDK. */ - public constructor(options: ReactNativeClientOptions) { - if (!options.transport) { - options.transport = (options: ReactNativeTransportOptions, nativeFetch?: FetchImpl): Transport => { - if (NATIVE.isNativeTransportAvailable()) { - return makeReactNativeTransport(options); - } - return makeFetchTransport(options, nativeFetch); - }; - } - options._metadata = options._metadata || {}; - options._metadata.sdk = options._metadata.sdk || defaultSdkInfo; - super(options); - - this._outcomesBuffer = []; + public constructor(options: ReactNativeClientOptions) { + if (!options.transport) { + options.transport = (options: ReactNativeTransportOptions, nativeFetch?: FetchImpl): Transport => { + if (NATIVE.isNativeTransportAvailable()) { + return makeReactNativeTransport(options); + } + return makeFetchTransport(options, nativeFetch); + }; + } + options._metadata = options._metadata || {}; + options._metadata.sdk = options._metadata.sdk || defaultSdkInfo; + super(options); + + this._outcomesBuffer = []; // This is a workaround for now using fetch on RN, this is a known issue in react-native and only generates a warning // YellowBox deprecated and replaced with with LogBox in RN 0.63 @@ -65,18 +65,8 @@ export class ReactNativeClient extends BaseClient { YellowBox.ignoreWarnings(['Require cycle:']); } - this._browserClient = new BrowserClient({ - dsn: options.dsn, - transport: options.transport, - transportOptions: options.transportOptions, - stackParser: options.stackParser || defaultStackParser, - integrations: [], - _metadata: options._metadata, - attachStacktrace: options.attachStacktrace, - }); - - void this._initNativeSdk(); - } + void this._initNativeSdk(); + } /** @@ -84,14 +74,36 @@ export class ReactNativeClient extends BaseClient { */ public eventFromException(exception: unknown, hint: EventHint = {}): PromiseLike { return Screenshot.attachScreenshotToEventHint(hint, this._options) - .then(enrichedHint => this._browserClient.eventFromException(exception, enrichedHint)); + .then(hintWithScreenshot => eventFromException( + this._options.stackParser, + exception, + hintWithScreenshot, + this._options.attachStacktrace, + )); } /** * @inheritDoc */ - public eventFromMessage(_message: string, _level?: SeverityLevel, _hint?: EventHint): PromiseLike { - return this._browserClient.eventFromMessage(_message, _level, _hint); + 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) { + 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; + }); } /** diff --git a/src/js/sdk.tsx b/src/js/sdk.tsx index 11e5998c74..a69d305e41 100644 --- a/src/js/sdk.tsx +++ b/src/js/sdk.tsx @@ -46,6 +46,7 @@ const DEFAULT_OPTIONS: ReactNativeOptions = { }, sendClientReports: true, maxQueueSize: DEFAULT_BUFFER_SIZE, + attachStacktrace: true, }; /** diff --git a/test/client.test.ts b/test/client.test.ts index ced3a2755f..172450cb3f 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -1,4 +1,5 @@ -import { Envelope, Event,Outcome, Transport } from '@sentry/types'; +import { defaultStackParser } from '@sentry/browser'; +import { Envelope, Event, Outcome, Transport } from '@sentry/types'; import { rejectedSyncPromise, SentryError } from '@sentry/utils'; import * as RN from 'react-native'; @@ -234,6 +235,39 @@ describe('Tests ReactNativeClient', () => { }); }); + describe('attachStacktrace', () => { + let mockTransportSend: jest.Mock; + let client: ReactNativeClient; + + beforeEach(() => { + mockTransportSend = jest.fn(() => Promise.resolve()); + client = new ReactNativeClient({ + ...DEFAULT_OPTIONS, + attachStacktrace: true, + stackParser: defaultStackParser, + dsn: EXAMPLE_DSN, + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + } as ReactNativeClientOptions); + }); + + afterEach(() => { + mockTransportSend.mockClear(); + }); + + const getMessageEventFrom = (func: jest.Mock) => + func.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload]; + + 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); + expect(getMessageEventFrom(mockTransportSend).exception).toBeUndefined(); + }); + }); + describe('envelopeHeader SdkInfo', () => { let mockTransportSend: jest.Mock; let client: ReactNativeClient;