Skip to content

Commit 4513fab

Browse files
V7: Uppercase boolean flags (#4965)
* initial changes * update changelog * add changelog ID * refactor and use event processor * split expo changes UI * fix lint * fix typo
1 parent e1d4577 commit 4513fab

15 files changed

Lines changed: 385 additions & 11 deletions

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,18 @@
1212

1313
- Automatically detect Release name and version for Expo Web ([#4967](https://github.com/getsentry/sentry-react-native/pull/4967))
1414

15+
### Breaking changes
16+
17+
- Tags formatting logic updated ([#4965](https://github.com/getsentry/sentry-react-native/pull/4965))
18+
Here are the altered/unaltered types, make sure to update your UI filters and alerts.
19+
20+
Unaltered: string, null, number, and undefined values remain unchanged.
21+
22+
Altered: Boolean values are now capitalized: true -> True, false -> False.
23+
1524
### Fixes
1625

26+
- tags with symbol are now logged ([#4965](https://github.com/getsentry/sentry-react-native/pull/4965))
1727
- ignoreError now filters Native errors ([#4948](https://github.com/getsentry/sentry-react-native/pull/4948))
1828

1929
You can use strings to filter errors or RegEx for filtering with a pattern.

packages/core/src/js/integrations/default.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
modulesLoaderIntegration,
2828
nativeLinkedErrorsIntegration,
2929
nativeReleaseIntegration,
30+
primitiveTagIntegration,
3031
reactNativeErrorHandlersIntegration,
3132
reactNativeInfoIntegration,
3233
screenshotIntegration,
@@ -153,5 +154,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
153154
integrations.push(debugSymbolicatorIntegration());
154155
}
155156

157+
integrations.push(primitiveTagIntegration());
158+
156159
return integrations;
157160
}

packages/core/src/js/integrations/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { createReactNativeRewriteFrames } from './rewriteframes';
2323
export { appRegistryIntegration } from './appRegistry';
2424
export { timeToDisplayIntegration } from '../tracing/integrations/timeToDisplayIntegration';
2525
export { breadcrumbsIntegration } from './breadcrumbs';
26+
export { primitiveTagIntegration } from './primitiveTagIntegration';
2627

2728
export {
2829
browserApiErrorsIntegration,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Integration, Primitive } from '@sentry/core';
2+
import { PrimitiveToString } from '../utils/primitiveConverter';
3+
import { NATIVE } from '../wrapper';
4+
5+
export const INTEGRATION_NAME = 'PrimitiveTagIntegration';
6+
7+
/**
8+
* Format tags set with Primitive values with a standard string format.
9+
*
10+
* When this Integration is enable, the following types will have the following behaviour:
11+
*
12+
* Unaltered: string, null, number, and undefined values remain unchanged.
13+
*
14+
* Altered:
15+
* Boolean values are now capitalized: true -> True, false -> False.
16+
* Symbols are stringified.
17+
*
18+
*/
19+
export const primitiveTagIntegration = (): Integration => {
20+
return {
21+
name: INTEGRATION_NAME,
22+
setup(client) {
23+
client.on('beforeSendEvent', event => {
24+
if (event.tags) {
25+
Object.keys(event.tags).forEach(key => {
26+
event.tags![key] = PrimitiveToString(event.tags![key]);
27+
});
28+
}
29+
});
30+
},
31+
afterAllSetup() {
32+
if (NATIVE.enableNative) {
33+
NATIVE._setPrimitiveProcessor((value: Primitive) => PrimitiveToString(value));
34+
}
35+
},
36+
};
37+
};

packages/core/src/js/integrations/reactnativeinfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function processEvent(event: Event, hint: EventHint): Event {
6060

6161
if (reactNativeContext.js_engine === 'hermes') {
6262
event.tags = {
63-
hermes: 'true',
63+
hermes: true,
6464
...event.tags,
6565
};
6666
}

packages/core/src/js/scopeSync.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ export function enableSyncToNative(scope: Scope): void {
2626
});
2727

2828
fillTyped(scope, 'setTag', original => (key, value): Scope => {
29-
NATIVE.setTag(key, value as string);
29+
NATIVE.setTag(key, NATIVE.primitiveProcessor(value));
3030
return original.call(scope, key, value);
3131
});
3232

3333
fillTyped(scope, 'setTags', original => (tags): Scope => {
3434
// As native only has setTag, we just loop through each tag key.
3535
Object.keys(tags).forEach(key => {
36-
NATIVE.setTag(key, tags[key] as string);
36+
NATIVE.setTag(key, NATIVE.primitiveProcessor(tags[key]));
3737
});
3838
return original.call(scope, tags);
3939
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Primitive } from '@sentry/core';
2+
3+
/**
4+
* Converts primitive to string.
5+
*/
6+
export function PrimitiveToString(primitive: Primitive): string | undefined {
7+
if (primitive === null) {
8+
return '';
9+
}
10+
11+
switch (typeof primitive) {
12+
case 'string':
13+
return primitive;
14+
case 'boolean':
15+
return primitive == true ? 'True' : 'False';
16+
case 'number':
17+
case 'bigint':
18+
return `${primitive}`;
19+
case 'undefined':
20+
return undefined;
21+
case 'symbol':
22+
return primitive.toString();
23+
default:
24+
return primitive as string;
25+
}
26+
}

packages/core/src/js/wrapper.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
EnvelopeItem,
77
Event,
88
Package,
9+
Primitive,
910
SeverityLevel,
1011
User,
1112
} from '@sentry/core';
@@ -66,6 +67,7 @@ interface SentryNativeWrapper {
6667
_NativeClientError: Error;
6768
_DisabledNativeError: Error;
6869

70+
_setPrimitiveProcessor: (processor: (value: Primitive) => void) => void;
6971
_processItem(envelopeItem: EnvelopeItem): EnvelopeItem;
7072
_processLevels(event: Event): Event;
7173
_processLevel(level: SeverityLevel): SeverityLevel;
@@ -95,7 +97,7 @@ interface SentryNativeWrapper {
9597
clearBreadcrumbs(): void;
9698
setExtra(key: string, extra: unknown): void;
9799
setUser(user: User | null): void;
98-
setTag(key: string, value: string): void;
100+
setTag(key: string, value?: string): void;
99101

100102
nativeCrash(): void;
101103

@@ -129,6 +131,8 @@ interface SentryNativeWrapper {
129131
setActiveSpanId(spanId: string): void;
130132

131133
encodeToBase64(data: Uint8Array): Promise<string | null>;
134+
135+
primitiveProcessor(value: Primitive): string;
132136
}
133137

134138
const EOL = encodeUTF8('\n');
@@ -396,7 +400,7 @@ export const NATIVE: SentryNativeWrapper = {
396400
* @param key string
397401
* @param value string
398402
*/
399-
setTag(key: string, value: string): void {
403+
setTag(key: string, value?: string): void {
400404
if (!this.enableNative) {
401405
return;
402406
}
@@ -777,6 +781,10 @@ export const NATIVE: SentryNativeWrapper = {
777781
}
778782
},
779783

784+
primitiveProcessor: function (value: Primitive): string {
785+
return value as string;
786+
},
787+
780788
/**
781789
* Gets the event from envelopeItem and applies the level filter to the selected event.
782790
* @param data An envelope item containing the event.
@@ -822,7 +830,6 @@ export const NATIVE: SentryNativeWrapper = {
822830
* @param event
823831
* @returns Event with more widely supported Severity level strings
824832
*/
825-
826833
_processLevels(event: Event): Event {
827834
const processed: Event = {
828835
...event,
@@ -841,7 +848,6 @@ export const NATIVE: SentryNativeWrapper = {
841848
* @param level
842849
* @returns More widely supported Severity level strings
843850
*/
844-
845851
_processLevel(level: SeverityLevel): SeverityLevel {
846852
if (level == ('log' as SeverityLevel)) {
847853
return 'debug' as SeverityLevel;
@@ -856,6 +862,10 @@ export const NATIVE: SentryNativeWrapper = {
856862
return !!module;
857863
},
858864

865+
_setPrimitiveProcessor: function (processor: (value: Primitive) => any): void {
866+
this.primitiveProcessor = processor;
867+
},
868+
859869
_DisabledNativeError: new SentryError('Native is disabled'),
860870

861871
_NativeClientError: new SentryError("Native Client is not available, can't start on native."),
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { Client } from '@sentry/core';
2+
import { primitiveTagIntegration } from '../../src/js/integrations/primitiveTagIntegration';
3+
import { NATIVE } from '../../src/js/wrapper';
4+
import { setupTestClient } from '../mocks/client';
5+
6+
describe('primitiveTagIntegration', () => {
7+
beforeEach(() => {
8+
jest.clearAllMocks();
9+
setupTestClient();
10+
});
11+
12+
afterEach(() => {
13+
jest.resetAllMocks();
14+
});
15+
16+
describe('integration setup', () => {
17+
it('sets up beforeSendEvent handler', () => {
18+
const integration = primitiveTagIntegration();
19+
const mockClient = {
20+
on: jest.fn(),
21+
} as any;
22+
23+
integration.setup!(mockClient);
24+
25+
expect(mockClient.on).toHaveBeenCalledWith('beforeSendEvent', expect.any(Function));
26+
});
27+
});
28+
29+
describe('beforeSendEvent processing', () => {
30+
let beforeSendEventHandler: (event: any) => void;
31+
32+
beforeEach(() => {
33+
const integration = primitiveTagIntegration();
34+
const mockClient = {
35+
on: jest.fn((eventName, handler) => {
36+
if (eventName === 'beforeSendEvent') {
37+
beforeSendEventHandler = handler;
38+
}
39+
}),
40+
} as any;
41+
42+
integration.setup!(mockClient);
43+
});
44+
45+
it('handles events without tags', () => {
46+
const event = { message: 'test' };
47+
48+
expect(() => beforeSendEventHandler(event)).not.toThrow();
49+
expect(event).toEqual({ message: 'test' });
50+
});
51+
52+
it('handles events with empty tags object', () => {
53+
const event = { tags: {} };
54+
55+
expect(() => beforeSendEventHandler(event)).not.toThrow();
56+
expect(event.tags).toEqual({});
57+
});
58+
59+
it('handles events with null tags', () => {
60+
const event = { tags: null };
61+
62+
expect(() => beforeSendEventHandler(event)).not.toThrow();
63+
expect(event.tags).toBeNull();
64+
});
65+
});
66+
67+
describe('integration with native processor', () => {
68+
it('sets primitiveProcessor to PrimitiveToString function', () => {
69+
const integration = primitiveTagIntegration();
70+
NATIVE.enableNative = true;
71+
jest.spyOn(NATIVE, '_setPrimitiveProcessor');
72+
73+
integration.afterAllSetup!({ getOptions: () => ({}) } as Client);
74+
75+
expect(NATIVE._setPrimitiveProcessor).toHaveBeenCalledWith(expect.any(Function));
76+
77+
// Verify the function passed is PrimitiveToString
78+
const passedFunction = (NATIVE._setPrimitiveProcessor as jest.Mock).mock.calls[0][0];
79+
expect(passedFunction(true)).toBe('True');
80+
expect(passedFunction(false)).toBe('False');
81+
expect(passedFunction(null)).toBe('');
82+
expect(passedFunction(42)).toBe('42');
83+
});
84+
85+
it('does not set processor when native is disabled', () => {
86+
const integration = primitiveTagIntegration();
87+
NATIVE.enableNative = false;
88+
jest.spyOn(NATIVE, '_setPrimitiveProcessor');
89+
90+
integration.afterAllSetup!({ getOptions: () => ({}) } as Client);
91+
92+
expect(NATIVE._setPrimitiveProcessor).not.toHaveBeenCalled();
93+
});
94+
});
95+
});

packages/core/test/integrations/reactnativeinfo.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('React Native Info', () => {
6060
},
6161
},
6262
tags: {
63-
hermes: 'true',
63+
hermes: true,
6464
},
6565
});
6666
});
@@ -71,7 +71,7 @@ describe('React Native Info', () => {
7171
const actualEvent = await executeIntegrationFor({}, {});
7272

7373
expectMocksToBeCalledOnce();
74-
expect(actualEvent?.tags?.hermes).toEqual('true');
74+
expect(actualEvent?.tags?.hermes).toBeTrue();
7575
expect(actualEvent?.contexts?.react_native_context).toEqual(
7676
expect.objectContaining({
7777
js_engine: 'hermes',

0 commit comments

Comments
 (0)