Skip to content

Commit 1ef8a04

Browse files
authored
Metrics for React Native SDK (#5402)
* Metrics for React Native SDK * Fixes # Conflicts: # samples/expo/package.json * Fixes, changelog entry � Conflicts: � CHANGELOG.md * Metrics changelog * Smallish fix * Lint fix * Fix * Metrics for sample apps
1 parent fdbea8b commit 1ef8a04

File tree

11 files changed

+279
-1
lines changed

11 files changed

+279
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Adds Metrics Beta ([#5402](https://github.com/getsentry/sentry-react-native/pull/5402))
14+
1115
### Fixes
1216

1317
- Fix `Object.freeze` type pollution from `@sentry-internal/replay` ([#5408](https://github.com/getsentry/sentry-react-native/issues/5408))

packages/core/src/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export {
4646
setCurrentClient,
4747
addEventProcessor,
4848
lastEventId,
49+
metrics,
4950
} from '@sentry/core';
5051

5152
export {

packages/core/src/js/wrapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export const NATIVE: SentryNativeWrapper = {
281281
beforeSend,
282282
beforeBreadcrumb,
283283
beforeSendTransaction,
284+
beforeSendMetric,
284285
integrations,
285286
ignoreErrors,
286287
logsOrigin,

packages/core/test/metrics.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { getClient, metrics, setCurrentClient } from '@sentry/core';
2+
import { ReactNativeClient } from '../src/js';
3+
import { getDefaultTestClientOptions } from './mocks/client';
4+
import { NATIVE } from './mockWrapper';
5+
6+
jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper'));
7+
8+
const EXAMPLE_DSN = 'https://[email protected]/148053';
9+
10+
describe('Metrics', () => {
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
jest.useFakeTimers();
14+
(NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => true);
15+
});
16+
17+
afterEach(() => {
18+
const client = getClient();
19+
client?.close();
20+
jest.clearAllTimers();
21+
jest.useRealTimers();
22+
});
23+
24+
describe('beforeSendMetric', () => {
25+
it('is called when enableMetrics is true and a metric is sent', async () => {
26+
const beforeSendMetric = jest.fn(metric => metric);
27+
28+
const client = new ReactNativeClient({
29+
...getDefaultTestClientOptions({
30+
dsn: EXAMPLE_DSN,
31+
enableMetrics: true,
32+
beforeSendMetric,
33+
}),
34+
});
35+
36+
setCurrentClient(client);
37+
client.init();
38+
39+
// Send a metric
40+
metrics.count('test_metric', 1);
41+
42+
jest.advanceTimersByTime(10000);
43+
expect(beforeSendMetric).toHaveBeenCalled();
44+
});
45+
46+
it('is not called when enableMetrics is false', async () => {
47+
const beforeSendMetric = jest.fn(metric => metric);
48+
49+
const client = new ReactNativeClient({
50+
...getDefaultTestClientOptions({
51+
dsn: EXAMPLE_DSN,
52+
enableMetrics: false,
53+
beforeSendMetric,
54+
}),
55+
});
56+
57+
setCurrentClient(client);
58+
client.init();
59+
60+
// Send a metric
61+
metrics.count('test_metric', 1);
62+
63+
jest.advanceTimersByTime(10000);
64+
expect(beforeSendMetric).not.toHaveBeenCalled();
65+
});
66+
67+
it('is called when enableMetrics is undefined (metrics are enabled by default)', async () => {
68+
const beforeSendMetric = jest.fn(metric => metric);
69+
70+
const client = new ReactNativeClient({
71+
...getDefaultTestClientOptions({
72+
dsn: EXAMPLE_DSN,
73+
beforeSendMetric,
74+
}),
75+
});
76+
77+
setCurrentClient(client);
78+
client.init();
79+
80+
// Send a metric
81+
metrics.count('test_metric', 1);
82+
83+
jest.advanceTimersByTime(10000);
84+
expect(beforeSendMetric).toHaveBeenCalled();
85+
});
86+
87+
it('allows beforeSendMetric to modify metrics when enableMetrics is true', async () => {
88+
const beforeSendMetric = jest.fn(metric => {
89+
// Modify the metric
90+
return { ...metric, name: 'modified_metric' };
91+
});
92+
93+
const client = new ReactNativeClient({
94+
...getDefaultTestClientOptions({
95+
dsn: EXAMPLE_DSN,
96+
enableMetrics: true,
97+
beforeSendMetric,
98+
}),
99+
});
100+
101+
setCurrentClient(client);
102+
client.init();
103+
104+
// Send a metric
105+
metrics.count('test_metric', 1);
106+
107+
jest.advanceTimersByTime(10000);
108+
expect(beforeSendMetric).toHaveBeenCalled();
109+
const modifiedMetric = beforeSendMetric.mock.results[0]?.value;
110+
expect(modifiedMetric).toBeDefined();
111+
expect(modifiedMetric.name).toBe('modified_metric');
112+
});
113+
114+
it('allows beforeSendMetric to drop metrics by returning null', async () => {
115+
const beforeSendMetric = jest.fn(() => null);
116+
117+
const client = new ReactNativeClient({
118+
...getDefaultTestClientOptions({
119+
dsn: EXAMPLE_DSN,
120+
enableMetrics: true,
121+
beforeSendMetric,
122+
}),
123+
});
124+
125+
setCurrentClient(client);
126+
client.init();
127+
128+
// Send a metric
129+
metrics.count('test_metric', 1);
130+
131+
// Advance timers
132+
jest.advanceTimersByTime(10000);
133+
expect(beforeSendMetric).toHaveBeenCalled();
134+
expect(beforeSendMetric.mock.results[0]?.value).toBeNull();
135+
});
136+
});
137+
138+
describe('metrics API', () => {
139+
it('metrics.count works when enableMetrics is true', () => {
140+
const client = new ReactNativeClient({
141+
...getDefaultTestClientOptions({
142+
dsn: EXAMPLE_DSN,
143+
enableMetrics: true,
144+
}),
145+
});
146+
147+
setCurrentClient(client);
148+
client.init();
149+
150+
expect(() => {
151+
metrics.count('test_metric', 1);
152+
}).not.toThrow();
153+
});
154+
155+
it('metrics can be sent with tags', async () => {
156+
const beforeSendMetric = jest.fn(metric => metric);
157+
158+
const client = new ReactNativeClient({
159+
...getDefaultTestClientOptions({
160+
dsn: EXAMPLE_DSN,
161+
enableMetrics: true,
162+
beforeSendMetric,
163+
}),
164+
});
165+
166+
setCurrentClient(client);
167+
client.init();
168+
169+
// Send a metric with tags
170+
metrics.count('test_metric', 1, {
171+
attributes: { environment: 'test' },
172+
});
173+
174+
jest.advanceTimersByTime(10000);
175+
expect(beforeSendMetric).toHaveBeenCalled();
176+
const sentMetric = beforeSendMetric.mock.calls[0]?.[0];
177+
expect(sentMetric).toBeDefined();
178+
});
179+
});
180+
});

packages/core/test/wrapper.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,24 @@ describe('Tests Native Wrapper', () => {
194194
expect(NATIVE.enableNative).toBe(true);
195195
});
196196

197+
test('filter beforeSendMetric when initializing Native SDK', async () => {
198+
await NATIVE.initNativeSdk({
199+
dsn: 'test',
200+
enableNative: true,
201+
autoInitializeNativeSdk: true,
202+
beforeSendMetric: jest.fn(),
203+
devServerUrl: undefined,
204+
defaultSidecarUrl: undefined,
205+
mobileReplayOptions: undefined,
206+
});
207+
208+
expect(RNSentry.initNativeSdk).toHaveBeenCalled();
209+
// @ts-expect-error mock value
210+
const initParameter = RNSentry.initNativeSdk.mock.calls[0][0];
211+
expect(initParameter).not.toHaveProperty('beforeSendMetric');
212+
expect(NATIVE.enableNative).toBe(true);
213+
});
214+
197215
test('filter integrations when initializing Native SDK', async () => {
198216
await NATIVE.initNativeSdk({
199217
dsn: 'test',

samples/expo/app/(tabs)/index.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,30 @@ export default function TabOneScreen() {
118118
}}
119119
/>
120120
</View>
121+
<View style={styles.buttonWrapper}>
122+
<Button
123+
title="Send count metric"
124+
onPress={() => {
125+
Sentry.metrics.count('count_metric', 1);
126+
}}
127+
/>
128+
</View>
129+
<View style={styles.buttonWrapper}>
130+
<Button
131+
title="Send distribution metric"
132+
onPress={() => {
133+
Sentry.metrics.count('distribution_metric', 100);
134+
}}
135+
/>
136+
</View>
137+
<View style={styles.buttonWrapper}>
138+
<Button
139+
title="Send count metric with attributes"
140+
onPress={() => {
141+
Sentry.metrics.count('count_metric', 1, { attributes: { from_test_app: true } });
142+
}}
143+
/>
144+
</View>
121145
<View style={styles.buttonWrapper}>
122146
<Button
123147
title="Flush"
@@ -202,7 +226,7 @@ export default function TabOneScreen() {
202226
Sentry.logger.warn('expo warn log');
203227
Sentry.logger.error('expo error log');
204228

205-
Sentry.logger.info('expo info log with data', { database: 'admin', number: 123, obj: { password: 'admin'} });
229+
Sentry.logger.info('expo info log with data', { database: 'admin', number: 123, obj: { password: 'admin' } });
206230
}}
207231
/>
208232
</View>

samples/expo/app/_layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ Sentry.init({
4040
console.log('Transaction beforeSend:', event.event_id);
4141
return event;
4242
},
43+
beforeSendMetric: (metric: Sentry.Metric) => {
44+
console.log('Metric beforeSend:', metric.name, metric.value);
45+
return metric;
46+
},
47+
enableMetrics: true,
4348
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
4449
onReady: ({ didCallNativeInit }) => {
4550
console.log('onReady called with didCallNativeInit:', didCallNativeInit);

samples/react-native-macos/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ Sentry.init({
5454
logWithoutTracing('Transaction beforeSend:', event.event_id);
5555
return event;
5656
},
57+
beforeSendMetric(metric: Sentry.Metric) {
58+
logWithoutTracing('Metric beforeSend:', metric.name, metric.value);
59+
return metric;
60+
},
5761
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
5862
onReady: ({ didCallNativeInit }) => {
5963
logWithoutTracing(

samples/react-native-macos/src/Screens/ErrorsScreen.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,24 @@ const ErrorsScreen = (_props: Props) => {
8686
setScopeProperties();
8787
}}
8888
/>
89+
<Button
90+
title="Send count metric"
91+
onPress={() => {
92+
Sentry.metrics.count('count_metric', 1);
93+
}}
94+
/>
95+
<Button
96+
title="Send distribution metric"
97+
onPress={() => {
98+
Sentry.metrics.count('distribution_metric', 100);
99+
}}
100+
/>
101+
<Button
102+
title="Send distribution metric"
103+
onPress={() => {
104+
Sentry.metrics.count('distribution_metric', 100);
105+
}}
106+
/>
89107
<Button
90108
title="Flush"
91109
onPress={async () => {

samples/react-native/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ Sentry.init({
5757
logWithoutTracing('Transaction beforeSend:', event.event_id);
5858
return event;
5959
},
60+
beforeSendMetric(metric: Sentry.Metric) {
61+
logWithoutTracing('Metric beforeSend:', metric.name, metric.value);
62+
return metric;
63+
},
6064
// This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted.
6165
onReady: ({ didCallNativeInit }) => {
6266
logWithoutTracing(
@@ -167,6 +171,7 @@ Sentry.init({
167171
// This should be disabled when manually initializing the native SDK
168172
// Note that options from JS are not passed to the native SDKs when initialized manually
169173
autoInitializeNativeSdk: true,
174+
enableMetrics: true,
170175
});
171176

172177
function BottomTabsNavigator() {

0 commit comments

Comments
 (0)