Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 44 additions & 32 deletions src/js/client.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -7,8 +7,10 @@ import {
Envelope,
Event,
EventHint,
Exception,
Outcome,
SeverityLevel,
Thread,
Transport,
UserFeedback,
} from '@sentry/types';
Expand All @@ -34,26 +36,24 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {

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
Expand All @@ -65,33 +65,45 @@ export class ReactNativeClient extends BaseClient<ReactNativeClientOptions> {
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();
}


/**
* @inheritDoc
*/
public eventFromException(exception: unknown, hint: EventHint = {}): PromiseLike<Event> {
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<Event> {
return this._browserClient.eventFromMessage(_message, _level, _hint);
public eventFromMessage(message: string, level?: SeverityLevel, hint?: EventHint): PromiseLike<Event> {
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;
});
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const DEFAULT_OPTIONS: ReactNativeOptions = {
},
sendClientReports: true,
maxQueueSize: DEFAULT_BUFFER_SIZE,
attachStacktrace: true,
};

/**
Expand Down
36 changes: 35 additions & 1 deletion test/client.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
Expand Down