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: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
<!-- prettier-ignore-end -->

## Unreleased

### Features

- Added `logsOrigin` to Sentry Options ([#5354](https://github.com/getsentry/sentry-react-native/pull/5354))
- You can now choose which logs are captured: 'native' for logs from native code only, 'js' for logs from the JavaScript layer only, or 'all' for both layers.
- Takes effect only if `enableLogs` is `true` and defaults to 'all', preserving previous behavior.

## 7.6.0


### Fixes

- Android SDK not being disabled when `options.enabled` is set to `false` ([#5334](https://github.com/getsentry/sentry-react-native/pull/5334))
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ export class ReactNativeClient extends Client<ReactNativeClientOptions> {
// We default this to true, as it is the safer scenario
options.parentSpanIsAlwaysRootSpan =
options.parentSpanIsAlwaysRootSpan === undefined ? true : options.parentSpanIsAlwaysRootSpan;

// enableLogs must be disabled before calling super() to avoid logs being captured.
// This makes a copy of the user defined value, so we can restore it later for the native usaege.
const originalEnableLogs = options.enableLogs;
if (options.enableLogs && options.logsOrigin === 'native') {
debug.log('disabling Sentry logs on JavaScript due to rule set by logsOrigin');
options.enableLogs = false;
}

super(options);

this._outcomesBuffer = [];
Expand All @@ -87,6 +96,9 @@ export class ReactNativeClient extends Client<ReactNativeClientOptions> {
}, DEFAULT_FLUSH_INTERVAL);
});
}

// Restore original settings for enabling Native options.
options.enableLogs = originalEnableLogs;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/js/integrations/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
if (options.enableNative) {
integrations.push(deviceContextIntegration());
integrations.push(modulesLoaderIntegration());
if (options.enableLogs) {
if (options.enableLogs && options.logsOrigin !== 'native') {
integrations.push(logEnricherIntegration());
integrations.push(consoleLoggingIntegration());
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/js/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,16 @@ export interface BaseReactNativeOptions {
* @default false
*/
propagateTraceparent?: boolean;

/**
* Controls which log origin is captured when `enableLogs` is set to true.
* 'all' will log all origins.
* 'js' will capture only JavaScript logs.
* 'native' will capture only native logs.
*
* @default 'all'
*/
logsOrigin?: 'all' | 'js' | 'native';
}

export type SentryReplayQuality = 'low' | 'medium' | 'high';
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ export const NATIVE: SentryNativeWrapper = {
enableNative: true,
autoInitializeNativeSdk: true,
...originalOptions,
// Keeps original behavior of enableLogs by not setting it when not defined.
...(originalOptions.enableLogs !== undefined
? { enableLogs: originalOptions.enableLogs && originalOptions.logsOrigin !== 'js' }
: {}),
};

if (!options.enableNative) {
Expand Down Expand Up @@ -273,8 +277,15 @@ export const NATIVE: SentryNativeWrapper = {

// filter out all the options that would crash native.
/* eslint-disable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */
const { beforeSend, beforeBreadcrumb, beforeSendTransaction, integrations, ignoreErrors, ...filteredOptions } =
options;
const {
beforeSend,
beforeBreadcrumb,
beforeSendTransaction,
integrations,
ignoreErrors,
logsOrigin,
...filteredOptions
} = options;
/* eslint-enable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */
const nativeIsReady = await RNSentry.initNativeSdk(filteredOptions);

Expand Down
45 changes: 45 additions & 0 deletions packages/core/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
Transport,
TransportMakeRequestResponse,
} from '@sentry/core';
import * as SentryCore from '@sentry/core';
import {
addAutoIpAddressToSession,
addAutoIpAddressToUser,
Expand Down Expand Up @@ -852,6 +853,50 @@ describe('Tests ReactNativeClient', () => {
);
});
});

describe('logger initialization', () => {
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
});

test('does not flush logs when enableLogs is false', () => {
jest.useFakeTimers();
const flushLogsSpy = jest.spyOn(SentryCore, '_INTERNAL_flushLogsBuffer').mockImplementation(jest.fn());

const { client } = createClientWithSpy({ enableLogs: false });

client.emit('afterCaptureLog', { message: 'test', attributes: {} } as unknown);
jest.advanceTimersByTime(5000);

expect(flushLogsSpy).not.toHaveBeenCalled();
});

test('does not flush logs when logsOrigin is native', () => {
jest.useFakeTimers();
const flushLogsSpy = jest.spyOn(SentryCore, '_INTERNAL_flushLogsBuffer').mockImplementation(jest.fn());

const { client } = createClientWithSpy({ enableLogs: true, logsOrigin: 'native' });

client.emit('afterCaptureLog', { message: 'test', attributes: {} } as unknown);
jest.advanceTimersByTime(5000);

expect(flushLogsSpy).not.toHaveBeenCalled();
});

it.each([['all' as const], ['js' as const]])('flushes logs when logsOrigin is %s', logOrlogsOriginigin => {
jest.useFakeTimers();
const flushLogsSpy = jest.spyOn(SentryCore, '_INTERNAL_flushLogsBuffer').mockImplementation(jest.fn());

const { client } = createClientWithSpy({ enableLogs: true, logsOrigin: logOrlogsOriginigin });

client.emit('afterCaptureLog', { message: 'test', attributes: {} } as unknown);
jest.advanceTimersByTime(5000);

expect(flushLogsSpy).toHaveBeenCalledTimes(1);
expect(flushLogsSpy).toHaveBeenLastCalledWith(client);
});
});
});

function mockedOptions(options: Partial<ReactNativeClientOptions>): ReactNativeClientOptions {
Expand Down
78 changes: 78 additions & 0 deletions packages/core/test/integrations/defaultLogs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { consoleLoggingIntegration } from '@sentry/browser';
import type { Integration } from '@sentry/core';
import { getDefaultIntegrations } from '../../src/js/integrations/default';
import { logEnricherIntegration } from '../../src/js/integrations/logEnricherIntegration';
import type { ReactNativeClientOptions } from '../../src/js/options';
import { notWeb } from '../../src/js/utils/environment';

jest.mock('../../src/js/utils/environment', () => {
const actual = jest.requireActual('../../src/js/utils/environment');
return {
...actual,
notWeb: jest.fn(() => true),
};
});

const logEnricherIntegrationName = logEnricherIntegration().name;
const consoleLoggingIntegrationName = consoleLoggingIntegration().name;

describe('getDefaultIntegrations - logging integrations', () => {
beforeEach(() => {
(notWeb as jest.Mock).mockReturnValue(true);
});

const createOptions = (overrides: Partial<ReactNativeClientOptions>): ReactNativeClientOptions => {
return {
dsn: 'https://example.com/1',
enableNative: true,
...overrides,
} as ReactNativeClientOptions;
};

const getIntegrationNames = (options: ReactNativeClientOptions): string[] => {
const integrations = getDefaultIntegrations(options);
return integrations.map((integration: Integration) => integration.name);
};

it('does not add logging integrations when enableLogs is falsy', () => {
const names = getIntegrationNames(createOptions({ enableLogs: false }));

expect(names).not.toContain(logEnricherIntegrationName);
expect(names).not.toContain(consoleLoggingIntegrationName);
});

it('adds logging integrations when enableLogs is true and logsOrigin is not native', () => {
const names = getIntegrationNames(createOptions({ enableLogs: true }));

expect(names).toContain(logEnricherIntegrationName);
expect(names).toContain(consoleLoggingIntegrationName);
});

it('does not add logging integrations when logsOrigin is native', () => {
const names = getIntegrationNames(
createOptions({
enableLogs: true,
logsOrigin: 'native' as unknown as ReactNativeClientOptions['logsOrigin'],
}),
);

expect(names).not.toContain(logEnricherIntegrationName);
expect(names).not.toContain(consoleLoggingIntegrationName);
});

it.each([
['all', true],
['js', true],
['native', false],
])('handles logsOrigin %s correctly', (logsOrigin, shouldInclude) => {
const names = getIntegrationNames(
createOptions({
enableLogs: true,
logsOrigin: logsOrigin as unknown as ReactNativeClientOptions['logsOrigin'],
}),
);

expect(names.includes(logEnricherIntegrationName)).toBe(shouldInclude);
expect(names.includes(consoleLoggingIntegrationName)).toBe(shouldInclude);
});
});
38 changes: 38 additions & 0 deletions packages/core/test/wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,44 @@ describe('Tests Native Wrapper', () => {
expect(initParameter.ignoreErrorsStr).toBeUndefined();
expect(initParameter.ignoreErrorsRegex).toBeUndefined();
});

test('does not set enableLogs when option is undefined', async () => {
await NATIVE.initNativeSdk({
dsn: 'test',
enableNative: true,
autoInitializeNativeSdk: true,
devServerUrl: undefined,
defaultSidecarUrl: undefined,
mobileReplayOptions: undefined,
});

expect(RNSentry.initNativeSdk).toHaveBeenCalled();
const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
expect(initParameter.enableLogs).toBeUndefined();
});

it.each([
['without logsOrigin', undefined, true],
['with logsOrigin set to Native', 'native' as const, true],
['with logsOrigin set to all', 'all' as const, true],
['with logsOrigin set to JS', 'js' as const, false],
])('handles enableLogs %s', async (_description, logsOrigin, expectedEnableLogs) => {
await NATIVE.initNativeSdk({
dsn: 'test',
enableNative: true,
autoInitializeNativeSdk: true,
enableLogs: true,
...(logsOrigin !== undefined ? { logsOrigin } : {}),
devServerUrl: undefined,
defaultSidecarUrl: undefined,
mobileReplayOptions: undefined,
});

expect(RNSentry.initNativeSdk).toHaveBeenCalled();
const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction<any>).mock.calls[0][0];
expect(initParameter.enableLogs).toBe(expectedEnableLogs);
expect(initParameter.logsOrigin).toBeUndefined();
});
});

describe('sendEnvelope', () => {
Expand Down
1 change: 1 addition & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Sentry.init({
_experiments: {
enableUnhandledCPPExceptionsV2: true,
},
logsOrigin: 'all',
enableLogs: true,
beforeSendLog: (log) => {
return log;
Expand Down
Loading