Skip to content

Commit 8cb8fe8

Browse files
Merge remote-tracking branch 'origin/main' into v6
2 parents 5e06e3e + ac41368 commit 8cb8fe8

File tree

11 files changed

+320
-13
lines changed

11 files changed

+320
-13
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44

55
### Fixes
66

7+
- TimetoTisplay correctly warns about not supporting the new React Native architecture ([#4160](https://github.com/getsentry/sentry-react-native/pull/4160))
8+
- Native Wrapper method `setContext` ensures only values convertible to NativeMap are passed ([#4168](https://github.com/getsentry/sentry-react-native/pull/4168))
9+
- Native Wrapper method `setExtra` ensures only stringified values are passed ([#4168](https://github.com/getsentry/sentry-react-native/pull/4168))
10+
- `setContext('key', null)` removes the key value also from platform context ([#4168](https://github.com/getsentry/sentry-react-native/pull/4168))
11+
12+
## 5.34.0
13+
14+
### Fixes
15+
716
- Handles error with string cause ([#4163](https://github.com/getsentry/sentry-react-native/pull/4163))
817
- Use `appLaunchedInForeground` to determine invalid app start data on Android ([#4146](https://github.com/getsentry/sentry-react-native/pull/4146))
918

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,18 +639,29 @@ public void clearBreadcrumbs() {
639639
}
640640

641641
public void setExtra(String key, String extra) {
642+
if (key == null || extra == null) {
643+
logger.log(SentryLevel.ERROR, "RNSentry.setExtra called with null key or value, can't change extra.");
644+
return;
645+
}
646+
642647
Sentry.configureScope(scope -> {
643648
scope.setExtra(key, extra);
644649
});
645650
}
646651

647652
public void setContext(final String key, final ReadableMap context) {
648-
if (key == null || context == null) {
653+
if (key == null) {
654+
logger.log(SentryLevel.ERROR, "RNSentry.setContext called with null key, can't change context.");
649655
return;
650656
}
657+
651658
Sentry.configureScope(scope -> {
652-
final HashMap<String, Object> contextHashMap = context.toHashMap();
659+
if (context == null) {
660+
scope.removeContexts(key);
661+
return;
662+
}
653663

664+
final HashMap<String, Object> contextHashMap = context.toHashMap();
654665
scope.setContexts(key, contextHashMap);
655666
});
656667
}

packages/core/ios/RNSentry.mm

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,8 +587,16 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
587587
context:(NSDictionary *)context
588588
)
589589
{
590+
if (key == nil) {
591+
return;
592+
}
593+
590594
[SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
591-
[scope setContextValue:context forKey:key];
595+
if (context == nil) {
596+
[scope removeContextForKey:key];
597+
} else {
598+
[scope setContextValue:context forKey:key];
599+
}
592600
}];
593601
}
594602

packages/core/src/js/tracing/timetodisplay.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Span,StartSpanOptions } from '@sentry/types';
33
import { fill, logger } from '@sentry/utils';
44
import * as React from 'react';
55

6+
import { isTurboModuleEnabled } from '../utils/environment';
67
import { SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from './origin';
78
import { getRNSentryOnDrawReporter, nativeComponentExists } from './timetodisplaynative';
89
import type {RNSentryOnDrawNextFrameEvent } from './timetodisplaynative.types';
@@ -60,12 +61,14 @@ function TimeToDisplay(props: {
6061
fullDisplay?: boolean;
6162
}): React.ReactElement {
6263
const RNSentryOnDrawReporter = getRNSentryOnDrawReporter();
64+
const isNewArchitecture = isTurboModuleEnabled();
6365

64-
if (__DEV__ && !nativeComponentMissingLogged && !nativeComponentExists) {
66+
if (__DEV__ && (isNewArchitecture || (!nativeComponentExists && !nativeComponentMissingLogged))){
6567
nativeComponentMissingLogged = true;
6668
// Using setTimeout with a delay of 0 milliseconds to defer execution and avoid printing the React stack trace.
6769
setTimeout(() => {
68-
logger.warn('TimeToInitialDisplay and TimeToFullDisplay are not supported on the web, Expo Go and New Architecture. Run native build or report an issue at https://github.com/getsentry/sentry-react-native');
70+
logger.warn(
71+
'TimeToInitialDisplay and TimeToFullDisplay are not supported on the web, Expo Go and New Architecture. Run native build or report an issue at https://github.com/getsentry/sentry-react-native');
6972
}, 0);
7073
}
7174

packages/core/src/js/wrapper.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type { NativeAndroidProfileEvent, NativeProfileEvent } from './profiling/
2828
import type { MobileReplayOptions } from './replay/mobilereplay';
2929
import type { RequiredKeysUser } from './user';
3030
import { isTurboModuleEnabled } from './utils/environment';
31+
import { convertToNormalizedObject } from './utils/normalize';
3132
import { ReactNativeLibraries } from './utils/rnlibraries';
3233
import { base64StringFromByteArray, utf8ToBytes } from './vendor';
3334

@@ -84,7 +85,8 @@ interface SentryNativeWrapper {
8485
enableNativeFramesTracking(): void;
8586

8687
addBreadcrumb(breadcrumb: Breadcrumb): void;
87-
setContext(key: string, context: { [key: string]: unknown } | null): void;
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
setContext(key: string, context: { [key: string]: any } | null): void;
8890
clearBreadcrumbs(): void;
8991
setExtra(key: string, extra: unknown): void;
9092
setUser(user: User | null): void;
@@ -396,10 +398,25 @@ export const NATIVE: SentryNativeWrapper = {
396398
throw this._NativeClientError;
397399
}
398400

399-
// we stringify the extra as native only takes in strings.
400-
const stringifiedExtra = typeof extra === 'string' ? extra : JSON.stringify(extra);
401+
if (typeof extra === 'string') {
402+
return RNSentry.setExtra(key, extra);
403+
}
404+
if (typeof extra === 'undefined') {
405+
return RNSentry.setExtra(key, 'undefined');
406+
}
407+
408+
let stringifiedExtra: string | undefined;
409+
try {
410+
const normalizedExtra = normalize(extra);
411+
stringifiedExtra = JSON.stringify(normalizedExtra);
412+
} catch (e) {
413+
logger.error('Extra for key ${key} not passed to native SDK, because it contains non-stringifiable values', e);
414+
}
401415

402-
RNSentry.setExtra(key, stringifiedExtra);
416+
if (typeof stringifiedExtra === 'string') {
417+
return RNSentry.setExtra(key, stringifiedExtra);
418+
}
419+
return RNSentry.setExtra(key, '**non-stringifiable**');
403420
},
404421

405422
/**
@@ -436,19 +453,35 @@ export const NATIVE: SentryNativeWrapper = {
436453
},
437454

438455
/**
439-
* Sets context on the native scope. Not implemented in Android yet.
456+
* Sets context on the native scope.
440457
* @param key string
441458
* @param context key-value map
442459
*/
443-
setContext(key: string, context: { [key: string]: unknown } | null): void {
460+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
461+
setContext(key: string, context: { [key: string]: any } | null): void {
444462
if (!this.enableNative) {
445463
return;
446464
}
447465
if (!this._isModuleLoaded(RNSentry)) {
448466
throw this._NativeClientError;
449467
}
450468

451-
RNSentry.setContext(key, context !== null ? normalize(context) : null);
469+
if (context === null) {
470+
return RNSentry.setContext(key, null);
471+
}
472+
473+
let normalizedContext: Record<string, unknown> | undefined;
474+
try {
475+
normalizedContext = convertToNormalizedObject(context);
476+
} catch (e) {
477+
logger.error('Context for key ${key} not passed to native SDK, because it contains non-serializable values', e);
478+
}
479+
480+
if (normalizedContext) {
481+
RNSentry.setContext(key, normalizedContext);
482+
} else {
483+
RNSentry.setContext(key, { error: '**non-serializable**' });
484+
}
452485
},
453486

454487
/**

packages/core/test/tracing/timetodisplay.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import { logger } from '@sentry/utils';
2+
jest.spyOn(logger, 'warn');
3+
14
import * as mockedtimetodisplaynative from './mockedtimetodisplaynative';
25
jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative);
36

7+
import { isTurboModuleEnabled } from '../../src/js/utils/environment';
8+
jest.mock('../../src/js/utils/environment', () => ({
9+
isTurboModuleEnabled: jest.fn().mockReturnValue(false),
10+
}));
11+
412
import { getActiveSpan, getCurrentScope, getGlobalScope, getIsolationScope, getSpanDescendants, setCurrentClient, spanToJSON, startSpanManual} from '@sentry/core';
513
import type { Event, Measurements, Span, SpanJSON} from '@sentry/types';
614
import * as React from "react";
@@ -371,6 +379,26 @@ describe('TimeToDisplay', () => {
371379
expect(spanToJSON(initialDisplaySpan!).timestamp).toEqual(initialDisplayEndTimestampMs / 1_000);
372380
expect(spanToJSON(fullDisplaySpan!).timestamp).toEqual(fullDisplayEndTimestampMs / 1_000);
373381
});
382+
383+
test('should not log a warning if native component exists and not in new architecture', async () => {
384+
385+
(isTurboModuleEnabled as jest.Mock).mockReturnValue(false);
386+
387+
TestRenderer.create(<TimeToInitialDisplay record={true} />);
388+
await jest.runOnlyPendingTimersAsync(); // Flush setTimeout.
389+
390+
expect(logger.warn).not.toHaveBeenCalled();
391+
});
392+
393+
test('should log a warning if in new architecture', async () => {
394+
395+
(isTurboModuleEnabled as jest.Mock).mockReturnValue(true);
396+
TestRenderer.create(<TimeToInitialDisplay record={true} />);
397+
await jest.runOnlyPendingTimersAsync(); // Flush setTimeout.
398+
399+
expect(logger.warn).toHaveBeenCalledWith(
400+
'TimeToInitialDisplay and TimeToFullDisplay are not supported on the web, Expo Go and New Architecture. Run native build or report an issue at https://github.com/getsentry/sentry-react-native');
401+
});
374402
});
375403

376404
function getInitialDisplaySpan(span?: Span) {

packages/core/test/wrapper.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,4 +629,122 @@ describe('Tests Native Wrapper', () => {
629629
expect(NATIVE.stopProfiling()).toBe(null);
630630
});
631631
});
632+
633+
describe('setExtra', () => {
634+
test('passes string value to native method', () => {
635+
NATIVE.setExtra('key', 'string value');
636+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'string value');
637+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
638+
});
639+
640+
test('stringifies number value before passing to native method', () => {
641+
NATIVE.setExtra('key', 42);
642+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', '42');
643+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
644+
});
645+
646+
test('stringifies boolean value before passing to native method', () => {
647+
NATIVE.setExtra('key', true);
648+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'true');
649+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
650+
});
651+
652+
test('stringifies object value before passing to native method', () => {
653+
const obj = { foo: 'bar', baz: 123 };
654+
NATIVE.setExtra('key', obj);
655+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', JSON.stringify(obj));
656+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
657+
});
658+
659+
test('stringifies array value before passing to native method', () => {
660+
const arr = [1, 'two', { three: 3 }];
661+
NATIVE.setExtra('key', arr);
662+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', JSON.stringify(arr));
663+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
664+
});
665+
666+
test('handles null value by stringifying', () => {
667+
NATIVE.setExtra('key', null);
668+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'null');
669+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
670+
});
671+
672+
test('handles undefined value by stringifying', () => {
673+
NATIVE.setExtra('key', undefined);
674+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', 'undefined');
675+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
676+
});
677+
678+
test('handles non-serializable value by stringifying', () => {
679+
const circular: { self?: unknown } = {};
680+
circular.self = circular;
681+
NATIVE.setExtra('key', circular);
682+
expect(RNSentry.setExtra).toHaveBeenCalledWith('key', '{"self":"[Circular ~]"}');
683+
expect(RNSentry.setExtra).toHaveBeenCalledOnce();
684+
});
685+
});
686+
687+
describe('setContext', () => {
688+
test('passes plain JS object to native method', () => {
689+
const context = { foo: 'bar', baz: 123 };
690+
NATIVE.setContext('key', context);
691+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', context);
692+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
693+
});
694+
695+
test('converts non-plain JS object to plain object before passing to native method', () => {
696+
class TestClass {
697+
prop = 'value';
698+
}
699+
const context = new TestClass();
700+
NATIVE.setContext('key', context);
701+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { prop: 'value' });
702+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
703+
});
704+
705+
test('converts array to object with "value" key before passing to native method', () => {
706+
const context = [1, 'two', { three: 3 }];
707+
NATIVE.setContext('key', context);
708+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: [1, 'two', { three: 3 }] });
709+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
710+
});
711+
712+
test('converts string primitive to object with "value" key before passing to native method', () => {
713+
NATIVE.setContext('key', 'string value' as unknown as object);
714+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: 'string value' });
715+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
716+
});
717+
718+
test('converts number primitive to object with "value" key before passing to native method', () => {
719+
NATIVE.setContext('key', 42 as unknown as object);
720+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: 42 });
721+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
722+
});
723+
724+
test('converts boolean primitive to object with "value" key before passing to native method', () => {
725+
NATIVE.setContext('key', true as unknown as object);
726+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: true });
727+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
728+
});
729+
730+
test('handles null value by passing null to native method', () => {
731+
NATIVE.setContext('key', null);
732+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', null);
733+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
734+
});
735+
736+
test('handles undefined value by converting to object with "value" key', () => {
737+
NATIVE.setContext('key', undefined as unknown as object);
738+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { value: undefined });
739+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
740+
});
741+
742+
test('handles non-serializable value by converting to normalized object', () => {
743+
const circular: { self?: unknown } = {};
744+
circular.self = circular;
745+
NATIVE.setContext('key', circular);
746+
expect(RNSentry.setContext).toHaveBeenCalledWith('key', { self: '[Circular ~]' });
747+
expect(RNSentry.setContext).toHaveBeenCalledOnce();
748+
});
749+
});
632750
});

samples/expo/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@
5858
]
5959
]
6060
}
61-
}
61+
}

samples/react-native/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import Ionicons from 'react-native-vector-icons/Ionicons';
3333
import PlaygroundScreen from './Screens/PlaygroundScreen';
3434
import { logWithoutTracing } from './utils';
3535
import { ErrorEvent } from '@sentry/types';
36+
import HeavyNavigationScreen from './Screens/HeavyNavigationScreen';
3637

3738
LogBox.ignoreAllLogs();
3839
const isMobileOs = Platform.OS === 'android' || Platform.OS === 'ios';
@@ -161,6 +162,10 @@ const TabTwoStack = Sentry.withProfiler(
161162
name="ManualTracker"
162163
component={ManualTrackerScreen}
163164
/>
165+
<Stack.Screen
166+
name="HeavyNavigation"
167+
component={HeavyNavigationScreen}
168+
/>
164169
<Stack.Screen
165170
name="PerformanceTiming"
166171
component={PerformanceTimingScreen}

0 commit comments

Comments
 (0)