Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
## Unreleased

### Features

- Adds metrics ([#5402](https://github.com/getsentry/sentry-react-native/pull/5402))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could mention that the feature is beta (similar to feedback and SR in the past)

Suggested change
- Adds metrics ([#5402](https://github.com/getsentry/sentry-react-native/pull/5402))
- Adds Metrics Beta ([#5402](https://github.com/getsentry/sentry-react-native/pull/5402))

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also a small snippet on how to use it would be nice here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antonis do we want to keep it as beta since it no longer is in beta for JavaScript?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would advocate towards shipping as beta since the product is still labeled as beta in JS sidebar

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can always differentiate between the state of the product, and the state of the SDK. The product is currently in Open Beta.


### Dependencies

- Bump Android SDK from v8.27.0 to v8.27.1 ([#5404](https://github.com/getsentry/sentry-react-native/pull/5404))
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export {
setCurrentClient,
addEventProcessor,
lastEventId,
metrics,
} from '@sentry/core';

export {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export const NATIVE: SentryNativeWrapper = {
beforeSend,
beforeBreadcrumb,
beforeSendTransaction,
beforeSendMetric,
integrations,
ignoreErrors,
logsOrigin,
Expand Down
180 changes: 180 additions & 0 deletions packages/core/test/metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { getClient, metrics, setCurrentClient } from '@sentry/core';
import { ReactNativeClient } from '../src/js';
import { getDefaultTestClientOptions } from './mocks/client';
import { NATIVE } from './mockWrapper';

jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper'));

const EXAMPLE_DSN = 'https://6890c2f6677340daa4804f8194804ea2@o19635.ingest.sentry.io/148053';

describe('Metrics', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
(NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => true);
});

afterEach(() => {
const client = getClient();
client?.close();
jest.clearAllTimers();
jest.useRealTimers();
});

describe('beforeSendMetric', () => {
it('is called when enableMetrics is true and a metric is sent', async () => {
const beforeSendMetric = jest.fn(metric => metric);

const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
enableMetrics: true,
beforeSendMetric,
}),
});

setCurrentClient(client);
client.init();

// Send a metric
metrics.count('test_metric', 1);

jest.advanceTimersByTime(10000);
expect(beforeSendMetric).toHaveBeenCalled();
});

it('is not called when enableMetrics is false', async () => {
const beforeSendMetric = jest.fn(metric => metric);

const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
enableMetrics: false,
beforeSendMetric,
}),
});

setCurrentClient(client);
client.init();

// Send a metric
metrics.count('test_metric', 1);

jest.advanceTimersByTime(10000);
expect(beforeSendMetric).not.toHaveBeenCalled();
});

it('is called when enableMetrics is undefined (metrics are enabled by default)', async () => {
const beforeSendMetric = jest.fn(metric => metric);

const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
beforeSendMetric,
}),
});

setCurrentClient(client);
client.init();

// Send a metric
metrics.count('test_metric', 1);

jest.advanceTimersByTime(10000);
expect(beforeSendMetric).toHaveBeenCalled();
});

it('allows beforeSendMetric to modify metrics when enableMetrics is true', async () => {
const beforeSendMetric = jest.fn(metric => {
// Modify the metric
return { ...metric, name: 'modified_metric' };
});

const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
enableMetrics: true,
beforeSendMetric,
}),
});

setCurrentClient(client);
client.init();

// Send a metric
metrics.count('test_metric', 1);

jest.advanceTimersByTime(10000);
expect(beforeSendMetric).toHaveBeenCalled();
const modifiedMetric = beforeSendMetric.mock.results[0]?.value;
expect(modifiedMetric).toBeDefined();
expect(modifiedMetric.name).toBe('modified_metric');
});

it('allows beforeSendMetric to drop metrics by returning null', async () => {
const beforeSendMetric = jest.fn(() => null);

const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
enableMetrics: true,
beforeSendMetric,
}),
});

setCurrentClient(client);
client.init();

// Send a metric
metrics.count('test_metric', 1);

// Advance timers
jest.advanceTimersByTime(10000);
expect(beforeSendMetric).toHaveBeenCalled();
expect(beforeSendMetric.mock.results[0]?.value).toBeNull();
});
});

describe('metrics API', () => {
it('metrics.count works when enableMetrics is true', () => {
const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
enableMetrics: true,
}),
});

setCurrentClient(client);
client.init();

expect(() => {
metrics.count('test_metric', 1);
}).not.toThrow();
});

it('metrics can be sent with tags', async () => {
const beforeSendMetric = jest.fn(metric => metric);

const client = new ReactNativeClient({
...getDefaultTestClientOptions({
dsn: EXAMPLE_DSN,
enableMetrics: true,
beforeSendMetric,
}),
});

setCurrentClient(client);
client.init();

// Send a metric with tags
metrics.count('test_metric', 1, {
attributes: { environment: 'test' },
});

jest.advanceTimersByTime(10000);
expect(beforeSendMetric).toHaveBeenCalled();
const sentMetric = beforeSendMetric.mock.calls[0]?.[0];
expect(sentMetric).toBeDefined();
});
});
});
Comment on lines +1 to +180
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite advances timers by 10 seconds (line 40, 62, etc.) expecting metrics to be sent, but there is no verification that the actual metric data reaches the transport layer or that the metrics aggregation completes. Consider adding assertions on the transport mock or verifying that NATIVE.sendEnvelope was called with the expected metric envelope. Additionally, ensure that ReactNativeClient properly inherits metrics support from @sentry/core Client.
Severity: MEDIUM

🤖 Prompt for AI Agent

Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/core/test/metrics.test.ts#L1-L180

Potential issue: The test suite advances timers by 10 seconds (line 40, 62, etc.)
expecting metrics to be sent, but there is no verification that the actual metric data
reaches the transport layer or that the metrics aggregation completes. Consider adding
assertions on the transport mock or verifying that `NATIVE.sendEnvelope` was called with
the expected metric envelope. Additionally, ensure that `ReactNativeClient` properly
inherits metrics support from `@sentry/core` Client.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 3443368

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point actually

18 changes: 18 additions & 0 deletions packages/core/test/wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ describe('Tests Native Wrapper', () => {
expect(NATIVE.enableNative).toBe(true);
});

test('filter beforeSendMetric when initializing Native SDK', async () => {
await NATIVE.initNativeSdk({
dsn: 'test',
enableNative: true,
autoInitializeNativeSdk: true,
beforeSendMetric: jest.fn(),
devServerUrl: undefined,
defaultSidecarUrl: undefined,
mobileReplayOptions: undefined,
});

expect(RNSentry.initNativeSdk).toHaveBeenCalled();
// @ts-expect-error mock value
const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
expect(initParameter).not.toHaveProperty('beforeSendMetric');
expect(NATIVE.enableNative).toBe(true);
});

test('filter integrations when initializing Native SDK', async () => {
await NATIVE.initNativeSdk({
dsn: 'test',
Expand Down
26 changes: 25 additions & 1 deletion samples/expo/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,30 @@ export default function TabOneScreen() {
}}
/>
</View>
<View style={styles.buttonWrapper}>
<Button
title="Send count metric"
onPress={() => {
Sentry.metrics.count('count_metric', 1);
}}
/>
</View>
<View style={styles.buttonWrapper}>
<Button
title="Send distribution metric"
onPress={() => {
Sentry.metrics.count('distribution_metric', 100);
}}
/>
</View>
<View style={styles.buttonWrapper}>
<Button
title="Send count metric with attributes"
onPress={() => {
Sentry.metrics.count('count_metric', 1, { attributes: { from_test_app: true } });
}}
/>
</View>
<View style={styles.buttonWrapper}>
<Button
title="Flush"
Expand Down Expand Up @@ -202,7 +226,7 @@ export default function TabOneScreen() {
Sentry.logger.warn('expo warn log');
Sentry.logger.error('expo error log');

Sentry.logger.info('expo info log with data', { database: 'admin', number: 123, obj: { password: 'admin'} });
Sentry.logger.info('expo info log with data', { database: 'admin', number: 123, obj: { password: 'admin' } });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated auto-formatting change

}}
/>
</View>
Expand Down
5 changes: 5 additions & 0 deletions samples/expo/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ Sentry.init({
console.log('Transaction beforeSend:', event.event_id);
return event;
},
beforeSendMetric: (metric: Sentry.Metric) => {
console.log('Metric beforeSend:', metric.name, metric.value);
return metric;
},
enableMetrics: true,
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
onReady: ({ didCallNativeInit }) => {
console.log('onReady called with didCallNativeInit:', didCallNativeInit);
Expand Down
4 changes: 4 additions & 0 deletions samples/react-native-macos/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Sentry.init({
logWithoutTracing('Transaction beforeSend:', event.event_id);
return event;
},
beforeSendMetric(metric: Sentry.Metric) {
logWithoutTracing('Metric beforeSend:', metric.name, metric.value);
return metric;
},
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
onReady: ({ didCallNativeInit }) => {
logWithoutTracing(
Expand Down
4 changes: 4 additions & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Sentry.init({
logWithoutTracing('Transaction beforeSend:', event.event_id);
return event;
},
beforeSendMetric(metric: Sentry.Metric) {
logWithoutTracing('Metric beforeSend:', metric.name, metric.value);
return metric;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdyt of enabling metrics for the sample app and adding an example in the app (e.g. a button or screen that uses metrics)

Suggested change
},
},
enableMetrics: true,

Copy link
Collaborator

@lucas-zimerman lucas-zimerman Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am surprised it is not on the experimental field
EDIT: it no longer is experimental

// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
onReady: ({ didCallNativeInit }) => {
logWithoutTracing(
Expand Down
Loading