From 24d04fbe1d1b5384f2e50351575f9136226374aa Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 2 Jul 2025 16:07:26 -0300 Subject: [PATCH 1/7] initial changes --- .../src/js/integrations/reactnativeinfo.ts | 2 +- packages/core/src/js/scopeSync.ts | 5 +- .../core/src/js/utils/primitiveConverter.ts | 26 ++++++++++ packages/core/src/js/wrapper.ts | 15 +++++- .../test/integrations/reactnativeinfo.test.ts | 4 +- .../test/utils/PrimitiveConverter.test.ts | 49 +++++++++++++++++++ packages/core/test/wrapper.test.ts | 30 ++++++++++++ .../react-native/src/Screens/ErrorsScreen.tsx | 43 ++++++++++++++++ 8 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/js/utils/primitiveConverter.ts create mode 100644 packages/core/test/utils/PrimitiveConverter.test.ts diff --git a/packages/core/src/js/integrations/reactnativeinfo.ts b/packages/core/src/js/integrations/reactnativeinfo.ts index a66ba9da2f..f45139f232 100644 --- a/packages/core/src/js/integrations/reactnativeinfo.ts +++ b/packages/core/src/js/integrations/reactnativeinfo.ts @@ -60,7 +60,7 @@ function processEvent(event: Event, hint: EventHint): Event { if (reactNativeContext.js_engine === 'hermes') { event.tags = { - hermes: 'true', + hermes: true, ...event.tags, }; } diff --git a/packages/core/src/js/scopeSync.ts b/packages/core/src/js/scopeSync.ts index 78ff027c20..ab8b6e3760 100644 --- a/packages/core/src/js/scopeSync.ts +++ b/packages/core/src/js/scopeSync.ts @@ -3,6 +3,7 @@ import { logger } from '@sentry/react'; import { DEFAULT_BREADCRUMB_LEVEL } from './breadcrumb'; import { fillTyped } from './utils/fill'; import { convertToNormalizedObject } from './utils/normalize'; +import { PrimitiveToString } from './utils/primitiveConverter'; import { NATIVE } from './wrapper'; /** @@ -26,14 +27,14 @@ export function enableSyncToNative(scope: Scope): void { }); fillTyped(scope, 'setTag', original => (key, value): Scope => { - NATIVE.setTag(key, value as string); + NATIVE.setTag(key, PrimitiveToString(value)); return original.call(scope, key, value); }); fillTyped(scope, 'setTags', original => (tags): Scope => { // As native only has setTag, we just loop through each tag key. Object.keys(tags).forEach(key => { - NATIVE.setTag(key, tags[key] as string); + NATIVE.setTag(key, PrimitiveToString(tags[key])); }); return original.call(scope, tags); }); diff --git a/packages/core/src/js/utils/primitiveConverter.ts b/packages/core/src/js/utils/primitiveConverter.ts new file mode 100644 index 0000000000..c57073c73f --- /dev/null +++ b/packages/core/src/js/utils/primitiveConverter.ts @@ -0,0 +1,26 @@ +import type { Primitive } from '@sentry/core'; + +/** + * Converts primitive to string. + */ +export function PrimitiveToString(primitive: Primitive): string | undefined { + if (primitive === null) { + return ''; + } + + switch (typeof primitive) { + case 'string': + return primitive; + case 'boolean': + return primitive == true ? 'True' : 'False'; + case 'number': + case 'bigint': + return `${primitive}`; + case 'undefined': + return undefined; + case 'symbol': + return primitive.toString(); + default: + return primitive as string; + } +} diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index f4cc5c9c15..e34cfc7425 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -6,6 +6,7 @@ import type { EnvelopeItem, Event, Package, + Primitive, SeverityLevel, User, } from '@sentry/core'; @@ -29,6 +30,7 @@ import type { RequiredKeysUser } from './user'; import { encodeUTF8 } from './utils/encode'; import { isTurboModuleEnabled } from './utils/environment'; import { convertToNormalizedObject } from './utils/normalize'; +import { PrimitiveToString } from './utils/primitiveConverter'; import { ReactNativeLibraries } from './utils/rnlibraries'; import { base64StringFromByteArray } from './vendor'; @@ -95,7 +97,7 @@ interface SentryNativeWrapper { clearBreadcrumbs(): void; setExtra(key: string, extra: unknown): void; setUser(user: User | null): void; - setTag(key: string, value: string): void; + setTag(key: string, value?: string): void; nativeCrash(): void; @@ -396,7 +398,7 @@ export const NATIVE: SentryNativeWrapper = { * @param key string * @param value string */ - setTag(key: string, value: string): void { + setTag(key: string, value?: string): void { if (!this.enableNative) { return; } @@ -824,8 +826,17 @@ export const NATIVE: SentryNativeWrapper = { */ _processLevels(event: Event): Event { + let tags: { [key: string]: Primitive } | undefined = undefined; + if (event.tags) { + tags = {}; + Object.keys(event.tags).forEach(key => { + tags![key] = PrimitiveToString(event.tags![key]); + }); + } + const processed: Event = { ...event, + tags: tags, level: event.level ? this._processLevel(event.level) : undefined, breadcrumbs: event.breadcrumbs?.map(breadcrumb => ({ ...breadcrumb, diff --git a/packages/core/test/integrations/reactnativeinfo.test.ts b/packages/core/test/integrations/reactnativeinfo.test.ts index 9c6e9b8fc0..2a6ab284be 100644 --- a/packages/core/test/integrations/reactnativeinfo.test.ts +++ b/packages/core/test/integrations/reactnativeinfo.test.ts @@ -60,7 +60,7 @@ describe('React Native Info', () => { }, }, tags: { - hermes: 'true', + hermes: true, }, }); }); @@ -71,7 +71,7 @@ describe('React Native Info', () => { const actualEvent = await executeIntegrationFor({}, {}); expectMocksToBeCalledOnce(); - expect(actualEvent?.tags?.hermes).toEqual('true'); + expect(actualEvent?.tags?.hermes).toBeTrue(); expect(actualEvent?.contexts?.react_native_context).toEqual( expect.objectContaining({ js_engine: 'hermes', diff --git a/packages/core/test/utils/PrimitiveConverter.test.ts b/packages/core/test/utils/PrimitiveConverter.test.ts new file mode 100644 index 0000000000..0583eccd73 --- /dev/null +++ b/packages/core/test/utils/PrimitiveConverter.test.ts @@ -0,0 +1,49 @@ +import { PrimitiveToString } from '../../src/js/utils/primitiveConverter'; + +describe('Primitive to String', () => { + it('Doesnt change strings', () => { + expect(PrimitiveToString('1234')).toBe('1234'); + expect(PrimitiveToString('1234,1')).toBe('1234,1'); + expect(PrimitiveToString('abc')).toBe('abc'); + }); + + it('Converts boolean to uppercase', () => { + expect(PrimitiveToString(false)).toBe('False'); + expect(PrimitiveToString(true)).toBe('True'); + }); + + it('Keeps undefined', () => { + expect(PrimitiveToString(undefined)).toBeUndefined(); + }); + + it('Converts null to empty', () => { + expect(PrimitiveToString(null)).toBe(''); + }); + + test.each([ + [0, '0'], + [1, '1'], + [12345, '12345'], + [Number.MIN_VALUE, `${Number.MIN_VALUE}`], + [Number.MAX_VALUE, `${Number.MAX_VALUE}`], + [Number.MIN_SAFE_INTEGER, `${Number.MIN_SAFE_INTEGER}`], + [Number.MAX_SAFE_INTEGER, `${Number.MAX_SAFE_INTEGER}`], + ])('Converts %p to "%s"', (input, expected) => { + expect(PrimitiveToString(input)).toBe(expected); + }); + + test.each([ + [BigInt('0'), '0'], + [BigInt('1'), '1'], + [BigInt('-1'), '-1'], + [BigInt('123456789012345678901234567890'), '123456789012345678901234567890'], + [BigInt('-98765432109876543210987654321'), '-98765432109876543210987654321'], + ])('converts bigint %p to "%s"', (input, expected) => { + expect(PrimitiveToString(input)).toBe(expected); + }); + + it('Symbol to String', () => { + const symbol = Symbol('a symbol'); + expect(PrimitiveToString(symbol)).toBe('Symbol(a symbol)'); + }); +}); diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index 72e29385fc..1af7e17899 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -631,6 +631,36 @@ describe('Tests Native Wrapper', () => { expect(NATIVE._processLevel('warning' as SeverityLevel)).toBe('warning' as SeverityLevel); expect(NATIVE._processLevel('error' as SeverityLevel)).toBe('error' as SeverityLevel); }); + + test('returns event unchanged if tags is undefined', () => { + const event: Event = { event_id: '5', tags: undefined }; + const processed = NATIVE._processLevels(event); + expect(processed.tags).toBeUndefined(); + }); + + test('returns event unchanged if no tags', () => { + const event: Event = { event_id: '1' }; + const processed = NATIVE._processLevels(event); + expect(processed.tags).toBeUndefined(); + }); + + test('returns event with empty tags if tags is empty object', () => { + const event: Event = { event_id: '2', tags: {} }; + const processed = NATIVE._processLevels(event); + expect(processed.tags).toEqual({}); + }); + + test('converts primitive tag values to strings', () => { + const event: Event = { event_id: '3', tags: { foo: 1, bar: true, baz: null } }; + const processed = NATIVE._processLevels(event); + expect(processed.tags).toEqual({ foo: '1', bar: 'True', baz: '' }); + }); + + test('converts mixed tag value types to strings', () => { + const event: Event = { event_id: '4', tags: { a: 'str', b: 42, c: false, d: undefined } }; + const processed = NATIVE._processLevels(event); + expect(processed.tags).toEqual({ a: 'str', b: '42', c: 'False', d: undefined }); + }); }); describe('closeNativeSdk', () => { diff --git a/samples/react-native/src/Screens/ErrorsScreen.tsx b/samples/react-native/src/Screens/ErrorsScreen.tsx index 26823aa3a0..cfce6f9c00 100644 --- a/samples/react-native/src/Screens/ErrorsScreen.tsx +++ b/samples/react-native/src/Screens/ErrorsScreen.tsx @@ -20,6 +20,7 @@ import { FallbackRender } from '@sentry/react'; import NativeSampleModule from '../../tm/NativeSampleModule'; import NativePlatformSampleModule from '../../tm/NativePlatformSampleModule'; import { TimeToFullDisplay } from '../utils'; +import type { Event as SentryEvent } from '@sentry/core'; const { AssetsModule, CppModule, CrashModule } = NativeModules; @@ -226,6 +227,48 @@ const ErrorsScreen = (_props: Props) => { } }} /> +