Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

## 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.

### 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
Binary file modified packages/core/android/libs/replay-stubs.jar
Binary file not shown.
10 changes: 10 additions & 0 deletions packages/core/src/js/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ 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;

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 +94,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