Skip to content

Commit ac41368

Browse files
fix: setContext correctly processes non-plain JS object (#4168)
Co-authored-by: Antonis Lilis <[email protected]>
1 parent d7800c3 commit ac41368

File tree

5 files changed

+183
-10
lines changed

5 files changed

+183
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
### Fixes
66

77
- 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))
811

912
## 5.34.0
1013

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
}

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

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;
@@ -395,10 +397,25 @@ export const NATIVE: SentryNativeWrapper = {
395397
throw this._NativeClientError;
396398
}
397399

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

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

404421
/**
@@ -435,19 +452,35 @@ export const NATIVE: SentryNativeWrapper = {
435452
},
436453

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

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

453486
/**

test/wrapper.test.ts

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

0 commit comments

Comments
 (0)