From 6d7f797063146ffa588ae70a8bde9cf6e4b20fef Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Apr 2026 15:49:00 +0200 Subject: [PATCH 1/3] feat(core): Expose scope-level attributes API Implement setAttribute, setAttributes, and removeAttribute bridging to native SDKs (Android 8.38.0+ and Cocoa 9.10.0+). These scope attributes are automatically included in structured logs and metrics. Closes #5764 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + .../io/sentry/react/RNSentryModuleImpl.java | 31 ++++++++------- .../java/io/sentry/react/RNSentryModule.java | 5 +++ .../java/io/sentry/react/RNSentryModule.java | 5 +++ packages/core/ios/RNSentry.mm | 20 ++++++---- packages/core/src/js/NativeRNSentry.ts | 1 + packages/core/src/js/scopeSync.ts | 38 +++++++++---------- packages/core/src/js/wrapper.ts | 16 ++++++++ packages/core/test/mockWrapper.ts | 1 + packages/core/test/scopeSync.test.ts | 16 +++++--- 10 files changed, 87 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d366965f5..8dfed45af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Expose scope-level attributes API (`setAttribute`, `setAttributes`, `removeAttribute`) bridging to native SDKs ([#5764](https://github.com/getsentry/sentry-react-native/issues/5764)) - Enable "Open Sentry" button in Playground for Expo apps ([#5947](https://github.com/getsentry/sentry-react-native/pull/5947)) - Add `attachAllThreads` option to attach full stack traces for all threads to captured events on iOS ([#5960](https://github.com/getsentry/sentry-react-native/issues/5960)) - Add `strictTraceContinuation` and `orgId` options for trace continuation validation ([#5829](https://github.com/getsentry/sentry-react-native/pull/5829)) 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'); + }); }); }); From b31db628f756e89a9947e5df88e20480d0c5e110 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Apr 2026 15:50:56 +0200 Subject: [PATCH 2/3] fix(changelog): Move scope attributes entry to Unreleased section Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dfed45af0..01150a8566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. -## 8.8.0 +## Unreleased ### Features - Expose scope-level attributes API (`setAttribute`, `setAttributes`, `removeAttribute`) bridging to native SDKs ([#5764](https://github.com/getsentry/sentry-react-native/issues/5764)) + +## 8.8.0 + +### Features + - Enable "Open Sentry" button in Playground for Expo apps ([#5947](https://github.com/getsentry/sentry-react-native/pull/5947)) - Add `attachAllThreads` option to attach full stack traces for all threads to captured events on iOS ([#5960](https://github.com/getsentry/sentry-react-native/issues/5960)) - Add `strictTraceContinuation` and `orgId` options for trace continuation validation ([#5829](https://github.com/getsentry/sentry-react-native/pull/5829)) From ef6988853e5688ab50a522d402c8508b323036e8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Apr 2026 15:58:21 +0200 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01150a8566..5b48c1e952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Features -- Expose scope-level attributes API (`setAttribute`, `setAttributes`, `removeAttribute`) bridging to native SDKs ([#5764](https://github.com/getsentry/sentry-react-native/issues/5764)) +- Expose scope-level attributes API (`setAttribute`, `setAttributes`, `removeAttribute`) bridging to native SDKs ([#6009](https://github.com/getsentry/sentry-react-native/pull/6009)) ## 8.8.0