Skip to content

Commit 06cc302

Browse files
committed
Scope attributes sync (without native)
1 parent 236f37a commit 06cc302

File tree

5 files changed

+137
-0
lines changed

5 files changed

+137
-0
lines changed

packages/core/src/js/NativeRNSentry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface Spec extends TurboModule {
3232
setContext(key: string, value: UnsafeObject | null): void;
3333
setExtra(key: string, value: string): void;
3434
setTag(key: string, value: string): void;
35+
setAttribute(key: string, value: string): void;
36+
setAttributes(attributes: UnsafeObject): void;
3537
enableNativeFramesTracking(): void;
3638
fetchModules(): Promise<string | undefined | null>;
3739
fetchViewHierarchy(): Promise<number[] | undefined | null>;

packages/core/src/js/scopeSync.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,28 @@ export function enableSyncToNative(scope: Scope): void {
7979
NATIVE.setContext(key, context);
8080
return original.call(scope, key, context);
8181
});
82+
83+
fillTyped(scope, 'setAttribute', original => (key: string, value: unknown): Scope => {
84+
// Only sync primitive types
85+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
86+
NATIVE.setAttribute(key, value);
87+
}
88+
return original.call(scope, key, value);
89+
});
90+
91+
fillTyped(scope, 'setAttributes', original => (attributes: Record<string, unknown>): Scope => {
92+
// Filter to only primitive types
93+
const primitiveAttrs: Record<string, string | number | boolean> = {};
94+
Object.keys(attributes).forEach(key => {
95+
const value = attributes[key];
96+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
97+
primitiveAttrs[key] = value;
98+
}
99+
});
100+
101+
if (Object.keys(primitiveAttrs).length > 0) {
102+
NATIVE.setAttributes(primitiveAttrs);
103+
}
104+
return original.call(scope, attributes);
105+
});
82106
}

packages/core/src/js/wrapper.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ interface SentryNativeWrapper {
100100
setExtra(key: string, extra: unknown): void;
101101
setUser(user: User | null): void;
102102
setTag(key: string, value?: string): void;
103+
setAttribute(key: string, value: string | number | boolean): void;
104+
setAttributes(attributes: Record<string, string | number | boolean>): void;
103105

104106
nativeCrash(): void;
105107

@@ -551,6 +553,43 @@ export const NATIVE: SentryNativeWrapper = {
551553
}
552554
},
553555

556+
/**
557+
* Sets an attribute on the native scope.
558+
* @param key string
559+
* @param value primitive value (string, number, or boolean)
560+
*/
561+
setAttribute(key: string, value: string | number | boolean): void {
562+
if (!this.enableNative) {
563+
return;
564+
}
565+
if (!this._isModuleLoaded(RNSentry)) {
566+
throw this._NativeClientError;
567+
}
568+
569+
const stringifiedValue = this.primitiveProcessor(value);
570+
RNSentry.setAttribute(key, stringifiedValue);
571+
},
572+
573+
/**
574+
* Sets multiple attributes on the native scope.
575+
* @param attributes key-value map of attributes (only string, number, and boolean values)
576+
*/
577+
setAttributes(attributes: Record<string, string | number | boolean>): void {
578+
if (!this.enableNative) {
579+
return;
580+
}
581+
if (!this._isModuleLoaded(RNSentry)) {
582+
throw this._NativeClientError;
583+
}
584+
585+
const serializedAttributes: Record<string, string> = {};
586+
Object.keys(attributes).forEach(key => {
587+
serializedAttributes[key] = this.primitiveProcessor(attributes[key]);
588+
});
589+
590+
RNSentry.setAttributes(serializedAttributes);
591+
},
592+
554593
/**
555594
* Closes the Native Layer SDK
556595
*/

packages/core/test/mockWrapper.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const NATIVE: MockInterface<NativeType> = {
4141
setExtra: jest.fn(),
4242
setUser: jest.fn(),
4343
setTag: jest.fn(),
44+
setAttribute: jest.fn(),
45+
setAttributes: jest.fn(),
4446

4547
nativeCrash: jest.fn(),
4648

@@ -92,6 +94,7 @@ NATIVE.crashedLastRun.mockResolvedValue(false);
9294
NATIVE.popTimeToDisplayFor.mockResolvedValue(null);
9395
NATIVE.getNewScreenTimeToDisplay.mockResolvedValue(null);
9496
NATIVE.primitiveProcessor.mockReturnValue('');
97+
NATIVE.fetchNativeLogAttributes = jest.fn().mockResolvedValue({});
9598
export const getRNSentryModule = jest.fn();
9699

97100
export { NATIVE };

packages/core/test/scopeSync.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ describe('ScopeSync', () => {
116116
let setExtrasScopeSpy: jest.SpyInstance;
117117
let addBreadcrumbScopeSpy: jest.SpyInstance;
118118
let setContextScopeSpy: jest.SpyInstance;
119+
let setAttributeScopeSpy: jest.SpyInstance;
120+
let setAttributesScopeSpy: jest.SpyInstance;
119121

120122
beforeAll(() => {
121123
const testScope = SentryCore.getIsolationScope();
@@ -126,6 +128,8 @@ describe('ScopeSync', () => {
126128
setExtrasScopeSpy = jest.spyOn(testScope, 'setExtras');
127129
addBreadcrumbScopeSpy = jest.spyOn(testScope, 'addBreadcrumb');
128130
setContextScopeSpy = jest.spyOn(testScope, 'setContext');
131+
setAttributeScopeSpy = jest.spyOn(testScope, 'setAttribute');
132+
setAttributesScopeSpy = jest.spyOn(testScope, 'setAttributes');
129133
});
130134

131135
beforeEach(() => {
@@ -214,5 +218,70 @@ describe('ScopeSync', () => {
214218
expect(NATIVE.setContext).toHaveBeenCalledExactlyOnceWith('key', { key: 'value' });
215219
expect(setContextScopeSpy).toHaveBeenCalledExactlyOnceWith('key', { key: 'value' });
216220
});
221+
222+
it('setAttribute', () => {
223+
expect(SentryCore.getIsolationScope().setAttribute).not.toBe(setAttributeScopeSpy);
224+
225+
SentryCore.getIsolationScope().setAttribute('session_id', 'abc123');
226+
expect(NATIVE.setAttribute).toHaveBeenCalledExactlyOnceWith('session_id', 'abc123');
227+
expect(setAttributeScopeSpy).toHaveBeenCalledExactlyOnceWith('session_id', 'abc123');
228+
});
229+
230+
it('setAttribute with number', () => {
231+
SentryCore.getIsolationScope().setAttribute('request_count', 42);
232+
expect(NATIVE.setAttribute).toHaveBeenCalledExactlyOnceWith('request_count', 42);
233+
});
234+
235+
it('setAttribute with boolean', () => {
236+
SentryCore.getIsolationScope().setAttribute('is_admin', true);
237+
expect(NATIVE.setAttribute).toHaveBeenCalledExactlyOnceWith('is_admin', true);
238+
});
239+
240+
it('setAttribute with non-primitive does not sync to native', () => {
241+
SentryCore.getIsolationScope().setAttribute('complex', { nested: 'object' });
242+
expect(NATIVE.setAttribute).not.toHaveBeenCalled();
243+
});
244+
245+
it('setAttributes', () => {
246+
expect(SentryCore.getIsolationScope().setAttributes).not.toBe(setAttributesScopeSpy);
247+
248+
SentryCore.getIsolationScope().setAttributes({
249+
session_type: 'test',
250+
request_count: 42,
251+
is_admin: true,
252+
});
253+
expect(NATIVE.setAttributes).toHaveBeenCalledExactlyOnceWith({
254+
session_type: 'test',
255+
request_count: 42,
256+
is_admin: true,
257+
});
258+
expect(setAttributesScopeSpy).toHaveBeenCalledExactlyOnceWith({
259+
session_type: 'test',
260+
request_count: 42,
261+
is_admin: true,
262+
});
263+
});
264+
265+
it('setAttributes filters non-primitive values', () => {
266+
SentryCore.getIsolationScope().setAttributes({
267+
session_type: 'test',
268+
request_count: 42,
269+
complex: { nested: 'object' },
270+
is_admin: true,
271+
});
272+
expect(NATIVE.setAttributes).toHaveBeenCalledExactlyOnceWith({
273+
session_type: 'test',
274+
request_count: 42,
275+
is_admin: true,
276+
});
277+
});
278+
279+
it('setAttributes does not sync to native if all values are non-primitive', () => {
280+
SentryCore.getIsolationScope().setAttributes({
281+
complex1: { nested: 'object' },
282+
complex2: ['array'],
283+
});
284+
expect(NATIVE.setAttributes).not.toHaveBeenCalled();
285+
});
217286
});
218287
});

0 commit comments

Comments
 (0)