diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abeb90664..d6b13bb2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Expose scope-level attributes API (`setAttribute`, `setAttributes`, `removeAttribute`) bridging to native SDKs ([#6009](https://github.com/getsentry/sentry-react-native/pull/6009)) - Warn Expo users at Metro startup when prebuilt native projects are missing Sentry configuration ([#5984](https://github.com/getsentry/sentry-react-native/pull/5984)) ### Dependencies diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index ec1e75607a..c8ee3abcd5 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -34,6 +34,7 @@ import io.sentry.ISerializer; import io.sentry.ScopesAdapter; import io.sentry.Sentry; +import io.sentry.SentryAttributes; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; import io.sentry.SentryExecutorService; @@ -677,23 +678,25 @@ public void setTag(String key, String value) { } public void setAttribute(String key, String value) { - // TODO(alwx): This is not implemented in sentry-android yet - /* - * Sentry.configureScope( - * scope -> { - * scope.setAttribute(key, value); - * }); - */ + Sentry.configureScope( + scope -> { + scope.setAttribute(key, value); + }); } public void setAttributes(ReadableMap attributes) { - // TODO(alwx): This is not implemented in sentry-android yet - /* - * Sentry.configureScope( - * scope -> { - * scope.setAttributes(attributes); - * }); - */ + Sentry.configureScope( + scope -> { + final Map attributesHashMap = attributes.toHashMap(); + scope.setAttributes(SentryAttributes.fromMap(attributesHashMap)); + }); + } + + public void removeAttribute(String key) { + Sentry.configureScope( + scope -> { + scope.removeAttribute(key); + }); } public void closeNativeSdk(Promise promise) { diff --git a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 9215c09c36..5ad03e30ca 100644 --- a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -128,6 +128,11 @@ public void setAttributes(ReadableMap attributes) { this.impl.setAttributes(attributes); } + @Override + public void removeAttribute(String key) { + this.impl.removeAttribute(key); + } + @Override public void closeNativeSdk(Promise promise) { this.impl.closeNativeSdk(promise); diff --git a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 85fdb97a35..6ca2fe2087 100644 --- a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -128,6 +128,11 @@ public void setAttributes(ReadableMap attributes) { this.impl.setAttributes(attributes); } + @ReactMethod + public void removeAttribute(String key) { + this.impl.removeAttribute(key); + } + @ReactMethod public void closeNativeSdk(Promise promise) { this.impl.closeNativeSdk(promise); diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index a43557079c..ec40b8f269 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -758,18 +758,22 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys RCT_EXPORT_METHOD(setAttribute : (NSString *)key value : (NSString *)value) { - // TODO(alwx): This is not implemented in sentry-cocoa yet - /*[SentrySDKWrapper - configureScope:^(SentryScope *_Nonnull scope) { [scope setAttribute:value forKey:key]; }];*/ + [SentrySDKWrapper configureScope:^( + SentryScope *_Nonnull scope) { [scope setAttributeValue:value forKey:key]; }]; } RCT_EXPORT_METHOD(setAttributes : (NSDictionary *)attributes) { - // TODO(alwx): This is not implemented in sentry-cocoa yet - /*[SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) { - [attributes enumerateKeysAndObjectsUsingBlock:^( - NSString *key, NSString *value, BOOL *stop) { [scope setAttribute:value forKey:key]; }]; - }];*/ + [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) { + [attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, + BOOL *stop) { [scope setAttributeValue:value forKey:key]; }]; + }]; +} + +RCT_EXPORT_METHOD(removeAttribute : (NSString *)key) +{ + [SentrySDKWrapper + configureScope:^(SentryScope *_Nonnull scope) { [scope removeAttributeForKey:key]; }]; } RCT_EXPORT_METHOD(crash) { [SentrySDKWrapper crash]; } diff --git a/packages/core/src/js/NativeRNSentry.ts b/packages/core/src/js/NativeRNSentry.ts index 0a4aff54b5..544df29cec 100644 --- a/packages/core/src/js/NativeRNSentry.ts +++ b/packages/core/src/js/NativeRNSentry.ts @@ -37,6 +37,7 @@ export interface Spec extends TurboModule { setTag(key: string, value: string): void; setAttribute(key: string, value: string): void; setAttributes(attributes: UnsafeObject): void; + removeAttribute(key: string): void; enableNativeFramesTracking(): void; fetchModules(): Promise; fetchViewHierarchy(): Promise; diff --git a/packages/core/src/js/scopeSync.ts b/packages/core/src/js/scopeSync.ts index abfd1dc069..2d3eac35c3 100644 --- a/packages/core/src/js/scopeSync.ts +++ b/packages/core/src/js/scopeSync.ts @@ -1,6 +1,5 @@ import type { Breadcrumb, Scope } from '@sentry/core'; -import { debug } from '@sentry/core'; import { logger } from '@sentry/react'; import { DEFAULT_BREADCRUMB_LEVEL } from './breadcrumb'; @@ -84,30 +83,31 @@ export function enableSyncToNative(scope: Scope): void { }); fillTyped(scope, 'setAttribute', original => (key: string, value: unknown): Scope => { - debug.warn('This feature is currently not supported.'); // Only sync primitive types - // Native layer still not supported - // if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - // NATIVE.setAttribute(key, value); - // } + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + NATIVE.setAttribute(key, value); + } return original.call(scope, key, value); }); fillTyped(scope, 'setAttributes', original => (attributes: Record): Scope => { - // Native layer not supported - debug.warn('This feature is currently not supported.'); // Filter to only primitive types - // const primitiveAttrs: Record = {}; - // Object.keys(attributes).forEach(key => { - // const value = attributes[key]; - // if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - // primitiveAttrs[key] = value; - // } - // }); - // - // if (Object.keys(primitiveAttrs).length > 0) { - // NATIVE.setAttributes(primitiveAttrs); - // } + const primitiveAttrs: Record = {}; + Object.keys(attributes).forEach(key => { + const value = attributes[key]; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + primitiveAttrs[key] = value; + } + }); + + if (Object.keys(primitiveAttrs).length > 0) { + NATIVE.setAttributes(primitiveAttrs); + } return original.call(scope, attributes); }); + + fillTyped(scope, 'removeAttribute', original => (key: string): Scope => { + NATIVE.removeAttribute(key); + return original.call(scope, key); + }); } diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index 1bee0fd232..874c07691e 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -108,6 +108,7 @@ interface SentryNativeWrapper { setTag(key: string, value?: string): void; setAttribute(key: string, value: string | number | boolean): void; setAttributes(attributes: Record): void; + removeAttribute(key: string): void; nativeCrash(): void; @@ -610,6 +611,21 @@ export const NATIVE: SentryNativeWrapper = { RNSentry.setAttributes(serializedAttributes); }, + /** + * Removes an attribute from the native scope. + * @param key string + */ + removeAttribute(key: string): void { + if (!this.enableNative) { + return; + } + if (!this._isModuleLoaded(RNSentry)) { + throw this._NativeClientError; + } + + RNSentry.removeAttribute(key); + }, + /** * Closes the Native Layer SDK */ diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts index afa8c6d1fb..86612bafa8 100644 --- a/packages/core/test/mockWrapper.ts +++ b/packages/core/test/mockWrapper.ts @@ -45,6 +45,7 @@ const NATIVE: MockInterface = { setTag: jest.fn(), setAttribute: jest.fn(), setAttributes: jest.fn(), + removeAttribute: jest.fn(), nativeCrash: jest.fn(), diff --git a/packages/core/test/scopeSync.test.ts b/packages/core/test/scopeSync.test.ts index fc6f8f354f..331f51a47d 100644 --- a/packages/core/test/scopeSync.test.ts +++ b/packages/core/test/scopeSync.test.ts @@ -118,11 +118,9 @@ describe('ScopeSync', () => { let setExtrasScopeSpy: jest.SpyInstance; let addBreadcrumbScopeSpy: jest.SpyInstance; let setContextScopeSpy: jest.SpyInstance; - /* - TODO: Uncomment once Native setattribute is implemented. let setAttributeScopeSpy: jest.SpyInstance; let setAttributesScopeSpy: jest.SpyInstance; - */ + let removeAttributeScopeSpy: jest.SpyInstance; beforeAll(() => { const testScope = SentryCore.getIsolationScope(); @@ -135,6 +133,7 @@ describe('ScopeSync', () => { setContextScopeSpy = jest.spyOn(testScope, 'setContext'); setAttributeScopeSpy = jest.spyOn(testScope, 'setAttribute'); setAttributesScopeSpy = jest.spyOn(testScope, 'setAttributes'); + removeAttributeScopeSpy = jest.spyOn(testScope, 'removeAttribute'); }); beforeEach(() => { @@ -224,8 +223,6 @@ describe('ScopeSync', () => { expect(setContextScopeSpy).toHaveBeenCalledExactlyOnceWith('key', { key: 'value' }); }); - /* - TODO: uncomment tests once native implementation is done. it('setAttribute', () => { expect(SentryCore.getIsolationScope().setAttribute).not.toBe(setAttributeScopeSpy); @@ -290,6 +287,13 @@ describe('ScopeSync', () => { }); expect(NATIVE.setAttributes).not.toHaveBeenCalled(); }); - */ + + it('removeAttribute', () => { + expect(SentryCore.getIsolationScope().removeAttribute).not.toBe(removeAttributeScopeSpy); + + SentryCore.getIsolationScope().removeAttribute('session_id'); + expect(NATIVE.removeAttribute).toHaveBeenCalledExactlyOnceWith('session_id'); + expect(removeAttributeScopeSpy).toHaveBeenCalledExactlyOnceWith('session_id'); + }); }); });