Skip to content

V7: Uppercase boolean flags #4965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,18 @@

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

### Breaking changes

- Tags formatting logic updated ([#4965](https://github.com/getsentry/sentry-react-native/pull/4965))
Here are the altered/unaltered types, make sure to update your UI filters and alerts.

Unaltered: string, null, number, and undefined values remain unchanged.

Altered: Boolean values are now capitalized: true -> True, false -> False.

### Fixes

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

You can use strings to filter errors or RegEx for filtering with a pattern.
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/js/integrations/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
modulesLoaderIntegration,
nativeLinkedErrorsIntegration,
nativeReleaseIntegration,
primitiveTagIntegration,
reactNativeErrorHandlersIntegration,
reactNativeInfoIntegration,
screenshotIntegration,
Expand Down Expand Up @@ -153,5 +154,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
integrations.push(debugSymbolicatorIntegration());
}

integrations.push(primitiveTagIntegration());

return integrations;
}
1 change: 1 addition & 0 deletions packages/core/src/js/integrations/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { createReactNativeRewriteFrames } from './rewriteframes';
export { appRegistryIntegration } from './appRegistry';
export { timeToDisplayIntegration } from '../tracing/integrations/timeToDisplayIntegration';
export { breadcrumbsIntegration } from './breadcrumbs';
export { primitiveTagIntegration } from './primitiveTagIntegration';

export {
browserApiErrorsIntegration,
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/js/integrations/primitiveTagIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Integration, Primitive } from '@sentry/core';
import { PrimitiveToString } from '../utils/primitiveConverter';
import { NATIVE } from '../wrapper';

export const INTEGRATION_NAME = 'PrimitiveTagIntegration';

/**
* Format tags set with Primitive values with a standard string format.
*
* When this Integration is enable, the following types will have the following behaviour:
*
* Unaltered: string, null, number, and undefined values remain unchanged.
*
* Altered:
* Boolean values are now capitalized: true -> True, false -> False.
* Symbols are stringified.
*
*/
export const primitiveTagIntegration = (): Integration => {
return {
name: INTEGRATION_NAME,
setup(client) {
client.on('beforeSendEvent', event => {
if (event.tags) {
Object.keys(event.tags).forEach(key => {
event.tags![key] = PrimitiveToString(event.tags![key]);
});
}
});
},
afterAllSetup() {
if (NATIVE.enableNative) {
NATIVE._setPrimitiveProcessor((value: Primitive) => PrimitiveToString(value));
}
},
};
};
2 changes: 1 addition & 1 deletion packages/core/src/js/integrations/reactnativeinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function processEvent(event: Event, hint: EventHint): Event {

if (reactNativeContext.js_engine === 'hermes') {
event.tags = {
hermes: 'true',
hermes: true,
...event.tags,
};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/js/scopeSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ export function enableSyncToNative(scope: Scope): void {
});

fillTyped(scope, 'setTag', original => (key, value): Scope => {
NATIVE.setTag(key, value as string);
NATIVE.setTag(key, NATIVE.primitiveProcessor(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, NATIVE.primitiveProcessor(tags[key]));
});
return original.call(scope, tags);
});
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/js/utils/primitiveConverter.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
18 changes: 14 additions & 4 deletions packages/core/src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
EnvelopeItem,
Event,
Package,
Primitive,
SeverityLevel,
User,
} from '@sentry/core';
Expand Down Expand Up @@ -66,6 +67,7 @@ interface SentryNativeWrapper {
_NativeClientError: Error;
_DisabledNativeError: Error;

_setPrimitiveProcessor: (processor: (value: Primitive) => void) => void;
_processItem(envelopeItem: EnvelopeItem): EnvelopeItem;
_processLevels(event: Event): Event;
_processLevel(level: SeverityLevel): SeverityLevel;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -129,6 +131,8 @@ interface SentryNativeWrapper {
setActiveSpanId(spanId: string): void;

encodeToBase64(data: Uint8Array): Promise<string | null>;

primitiveProcessor(value: Primitive): string;
}

const EOL = encodeUTF8('\n');
Expand Down Expand Up @@ -396,7 +400,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;
}
Expand Down Expand Up @@ -777,6 +781,10 @@ export const NATIVE: SentryNativeWrapper = {
}
},

primitiveProcessor: function (value: Primitive): string {
return value as string;
},

/**
* Gets the event from envelopeItem and applies the level filter to the selected event.
* @param data An envelope item containing the event.
Expand Down Expand Up @@ -822,7 +830,6 @@ export const NATIVE: SentryNativeWrapper = {
* @param event
* @returns Event with more widely supported Severity level strings
*/

_processLevels(event: Event): Event {
const processed: Event = {
...event,
Expand All @@ -841,7 +848,6 @@ export const NATIVE: SentryNativeWrapper = {
* @param level
* @returns More widely supported Severity level strings
*/

_processLevel(level: SeverityLevel): SeverityLevel {
if (level == ('log' as SeverityLevel)) {
return 'debug' as SeverityLevel;
Expand All @@ -856,6 +862,10 @@ export const NATIVE: SentryNativeWrapper = {
return !!module;
},

_setPrimitiveProcessor: function (processor: (value: Primitive) => any): void {
this.primitiveProcessor = processor;
},

_DisabledNativeError: new SentryError('Native is disabled'),

_NativeClientError: new SentryError("Native Client is not available, can't start on native."),
Expand Down
95 changes: 95 additions & 0 deletions packages/core/test/integrations/primitiveTagIntegration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Client } from '@sentry/core';
import { primitiveTagIntegration } from '../../src/js/integrations/primitiveTagIntegration';
import { NATIVE } from '../../src/js/wrapper';
import { setupTestClient } from '../mocks/client';

describe('primitiveTagIntegration', () => {
beforeEach(() => {
jest.clearAllMocks();
setupTestClient();
});

afterEach(() => {
jest.resetAllMocks();
});

describe('integration setup', () => {
it('sets up beforeSendEvent handler', () => {
const integration = primitiveTagIntegration();
const mockClient = {
on: jest.fn(),
} as any;

integration.setup!(mockClient);

expect(mockClient.on).toHaveBeenCalledWith('beforeSendEvent', expect.any(Function));
});
});

describe('beforeSendEvent processing', () => {
let beforeSendEventHandler: (event: any) => void;

beforeEach(() => {
const integration = primitiveTagIntegration();
const mockClient = {
on: jest.fn((eventName, handler) => {
if (eventName === 'beforeSendEvent') {
beforeSendEventHandler = handler;
}
}),
} as any;

integration.setup!(mockClient);
});

it('handles events without tags', () => {
const event = { message: 'test' };

expect(() => beforeSendEventHandler(event)).not.toThrow();
expect(event).toEqual({ message: 'test' });
});

it('handles events with empty tags object', () => {
const event = { tags: {} };

expect(() => beforeSendEventHandler(event)).not.toThrow();
expect(event.tags).toEqual({});
});

it('handles events with null tags', () => {
const event = { tags: null };

expect(() => beforeSendEventHandler(event)).not.toThrow();
expect(event.tags).toBeNull();
});
});

describe('integration with native processor', () => {
it('sets primitiveProcessor to PrimitiveToString function', () => {
const integration = primitiveTagIntegration();
NATIVE.enableNative = true;
jest.spyOn(NATIVE, '_setPrimitiveProcessor');

integration.afterAllSetup!({ getOptions: () => ({}) } as Client);

expect(NATIVE._setPrimitiveProcessor).toHaveBeenCalledWith(expect.any(Function));

// Verify the function passed is PrimitiveToString
const passedFunction = (NATIVE._setPrimitiveProcessor as jest.Mock).mock.calls[0][0];
expect(passedFunction(true)).toBe('True');
expect(passedFunction(false)).toBe('False');
expect(passedFunction(null)).toBe('');
expect(passedFunction(42)).toBe('42');
});

it('does not set processor when native is disabled', () => {
const integration = primitiveTagIntegration();
NATIVE.enableNative = false;
jest.spyOn(NATIVE, '_setPrimitiveProcessor');

integration.afterAllSetup!({ getOptions: () => ({}) } as Client);

expect(NATIVE._setPrimitiveProcessor).not.toHaveBeenCalled();
});
});
});
4 changes: 2 additions & 2 deletions packages/core/test/integrations/reactnativeinfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('React Native Info', () => {
},
},
tags: {
hermes: 'true',
hermes: true,
},
});
});
Expand All @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion packages/core/test/mockWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const NATIVE: MockInterface<NativeType> = {
_processLevel: jest.fn(),
_serializeObject: jest.fn(),
_isModuleLoaded: <NativeType['_isModuleLoaded'] & jest.Mock>jest.fn(),
_setPrimitiveProcessor: jest.fn(),

isNativeAvailable: jest.fn(),

Expand Down Expand Up @@ -63,6 +64,7 @@ const NATIVE: MockInterface<NativeType> = {
popTimeToDisplayFor: jest.fn(),
setActiveSpanId: jest.fn(),
encodeToBase64: jest.fn(),
primitiveProcessor: jest.fn(),
};

NATIVE.isNativeAvailable.mockReturnValue(true);
Expand All @@ -89,7 +91,7 @@ NATIVE.getCurrentReplayId.mockReturnValue(null);
NATIVE.crashedLastRun.mockResolvedValue(false);
NATIVE.popTimeToDisplayFor.mockResolvedValue(null);
NATIVE.getNewScreenTimeToDisplay.mockResolvedValue(null);

NATIVE.primitiveProcessor.mockReturnValue('');
export const getRNSentryModule = jest.fn();

export { NATIVE };
3 changes: 2 additions & 1 deletion packages/core/test/scopeSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,14 @@ describe('ScopeSync', () => {

it('setUser', () => {
expect(SentryCore.getIsolationScope().setUser).not.toBe(setUserScopeSpy);

const user = { id: '123' };
SentryCore.setUser(user);
expect(NATIVE.setUser).toHaveBeenCalledExactlyOnceWith({ id: '123' });
expect(setUserScopeSpy).toHaveBeenCalledExactlyOnceWith({ id: '123' });
});

it('setTag', () => {
jest.spyOn(NATIVE, 'primitiveProcessor').mockImplementation((value: SentryCore.Primitive) => value as string);
expect(SentryCore.getIsolationScope().setTag).not.toBe(setTagScopeSpy);

SentryCore.setTag('key', 'value');
Expand All @@ -151,6 +151,7 @@ describe('ScopeSync', () => {
});

it('setTags', () => {
jest.spyOn(NATIVE, 'primitiveProcessor').mockImplementation((value: SentryCore.Primitive) => value as string);
expect(SentryCore.getIsolationScope().setTags).not.toBe(setTagsScopeSpy);

SentryCore.setTags({ key: 'value', second: 'bar' });
Expand Down
Loading
Loading