diff --git a/CHANGELOG.md b/CHANGELOG.md index 3225aa8b4b..14daac2ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- Add support for applying scope attributes to logs ([#5579](https://github.com/getsentry/sentry-react-native/pull/5579)) - Add experimental `sentry-span-attributes` prop to attach custom attributes to user interaction spans ([#5569](https://github.com/getsentry/sentry-react-native/pull/5569)) ```tsx { + * 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); + * }); + */ + } + public void closeNativeSdk(Promise promise) { Sentry.close(); 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 993969d830..b928d2d9c4 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 @@ -112,6 +112,16 @@ public void setTag(String key, String value) { this.impl.setTag(key, value); } + @Override + public void setAttribute(String key, String value) { + this.impl.setAttribute(key, value); + } + + @Override + public void setAttributes(ReadableMap attributes) { + this.impl.setAttributes(attributes); + } + @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 103afeb890..0488e143c9 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 @@ -112,6 +112,16 @@ public void setTag(String key, String value) { this.impl.setTag(key, value); } + @ReactMethod + public void setAttribute(String key, String value) { + this.impl.setAttribute(key, value); + } + + @ReactMethod + public void setAttributes(ReadableMap attributes) { + this.impl.setAttributes(attributes); + } + @ReactMethod public void closeNativeSdk(Promise promise) { this.impl.closeNativeSdk(promise); @@ -132,6 +142,11 @@ public void fetchNativeDeviceContexts(Promise promise) { this.impl.fetchNativeDeviceContexts(promise); } + @ReactMethod + public void fetchNativeLogAttributes(Promise promise) { + this.impl.fetchNativeLogAttributes(promise); + } + @ReactMethod public void fetchNativeSdkInfo(Promise promise) { this.impl.fetchNativeSdkInfo(promise); diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index f5fefc0cd6..3461af87af 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -723,6 +723,22 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys configureScope:^(SentryScope *_Nonnull scope) { [scope setTagValue:value forKey:key]; }]; } +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]; }];*/ +} + +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]; }]; + }];*/ +} + RCT_EXPORT_METHOD(crash) { [SentrySDKWrapper crash]; } RCT_EXPORT_METHOD( diff --git a/packages/core/src/js/NativeRNSentry.ts b/packages/core/src/js/NativeRNSentry.ts index 3e75967c27..afd8fba03d 100644 --- a/packages/core/src/js/NativeRNSentry.ts +++ b/packages/core/src/js/NativeRNSentry.ts @@ -32,6 +32,8 @@ export interface Spec extends TurboModule { setContext(key: string, value: UnsafeObject | null): void; setExtra(key: string, value: string): void; setTag(key: string, value: string): void; + setAttribute(key: string, value: string): void; + setAttributes(attributes: UnsafeObject): void; enableNativeFramesTracking(): void; fetchModules(): Promise; fetchViewHierarchy(): Promise; diff --git a/packages/core/src/js/integrations/logEnricherIntegration.ts b/packages/core/src/js/integrations/logEnricherIntegration.ts index 285365cc77..98b3524e58 100644 --- a/packages/core/src/js/integrations/logEnricherIntegration.ts +++ b/packages/core/src/js/integrations/logEnricherIntegration.ts @@ -1,6 +1,6 @@ /* eslint-disable complexity */ import type { Integration, Log } from '@sentry/core'; -import { debug } from '@sentry/core'; +import { debug, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import type { ReactNativeClient } from '../client'; import { NATIVE } from '../wrapper'; @@ -33,7 +33,7 @@ let NativeCache: Record | undefined = undefined; * * @param logAttributes - The log attributes object to modify. * @param key - The attribute key to set. - * @param value - The value to set (only sets if truthy and key not present). + * @param value - The value to set (only sets if not null/undefined and key not present). * @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true. */ function setLogAttribute( @@ -42,7 +42,7 @@ function setLogAttribute( value: unknown, setEvenIfPresent = true, ): void { - if (value && (!logAttributes[key] || setEvenIfPresent)) { + if (value != null && (!logAttributes[key] || setEvenIfPresent)) { logAttributes[key] = value; } } @@ -79,6 +79,13 @@ function processLog(log: Log, client: ReactNativeClient): void { // Save log.attributes to a new variable const logAttributes = log.attributes ?? {}; + // Apply scope attributes from all active scopes (global, isolation, and current) + // These are applied first so they can be overridden by more specific attributes + const scopeAttributes = collectScopeAttributes(); + Object.keys(scopeAttributes).forEach((key: string) => { + setLogAttribute(logAttributes, key, scopeAttributes[key], false); + }); + // Use setLogAttribute with the variable instead of direct assignment setLogAttribute(logAttributes, 'device.brand', NativeCache.brand); setLogAttribute(logAttributes, 'device.model', NativeCache.model); @@ -93,3 +100,42 @@ function processLog(log: Log, client: ReactNativeClient): void { // Set log.attributes to the variable log.attributes = logAttributes; } + +/** + * Extracts primitive attributes from a scope and merges them into the target object. + * Only string, number, and boolean attribute values are included. + * + * @param scope - The scope to extract attributes from + * @param target - The target object to merge attributes into + */ +function extractScopeAttributes( + scope: ReturnType, + target: Record, +): void { + if (scope && typeof scope.getScopeData === 'function') { + const scopeAttrs = scope?.getScopeData?.().attributes || {}; + for (const [key, value] of Object.entries(scopeAttrs)) { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + target[key] = value; + } + } + } +} + +/** + * Collects attributes from all active scopes (global, isolation, and current). + * Only string, number, and boolean attribute values are supported. + * Attributes are merged in order of precedence: global < isolation < current. + * + * @returns A merged object containing all scope attributes. + */ +function collectScopeAttributes(): Record { + const attributes: Record = {}; + + // Collect attributes from all scopes in order of precedence + extractScopeAttributes(getGlobalScope(), attributes); + extractScopeAttributes(getIsolationScope(), attributes); + extractScopeAttributes(getCurrentScope(), attributes); + + return attributes; +} diff --git a/packages/core/src/js/scopeSync.ts b/packages/core/src/js/scopeSync.ts index 5c6537d89e..1fe6b65265 100644 --- a/packages/core/src/js/scopeSync.ts +++ b/packages/core/src/js/scopeSync.ts @@ -79,4 +79,28 @@ export function enableSyncToNative(scope: Scope): void { NATIVE.setContext(key, context); return original.call(scope, key, context); }); + + fillTyped(scope, 'setAttribute', original => (key: string, value: unknown): Scope => { + // Only sync primitive types + 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 => { + // 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); + } + return original.call(scope, attributes); + }); } diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index 4089bc5c75..55fcb3a2e8 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -100,6 +100,8 @@ interface SentryNativeWrapper { setExtra(key: string, extra: unknown): void; setUser(user: User | null): void; setTag(key: string, value?: string): void; + setAttribute(key: string, value: string | number | boolean): void; + setAttributes(attributes: Record): void; nativeCrash(): void; @@ -551,6 +553,43 @@ export const NATIVE: SentryNativeWrapper = { } }, + /** + * Sets an attribute on the native scope. + * @param key string + * @param value primitive value (string, number, or boolean) + */ + setAttribute(key: string, value: string | number | boolean): void { + if (!this.enableNative) { + return; + } + if (!this._isModuleLoaded(RNSentry)) { + throw this._NativeClientError; + } + + const stringifiedValue = this.primitiveProcessor(value); + RNSentry.setAttribute(key, stringifiedValue); + }, + + /** + * Sets multiple attributes on the native scope. + * @param attributes key-value map of attributes (only string, number, and boolean values) + */ + setAttributes(attributes: Record): void { + if (!this.enableNative) { + return; + } + if (!this._isModuleLoaded(RNSentry)) { + throw this._NativeClientError; + } + + const serializedAttributes: Record = {}; + Object.keys(attributes).forEach(key => { + serializedAttributes[key] = this.primitiveProcessor(attributes[key]); + }); + + RNSentry.setAttributes(serializedAttributes); + }, + /** * Closes the Native Layer SDK */ diff --git a/packages/core/test/integrations/logEnricherIntegration.test.ts b/packages/core/test/integrations/logEnricherIntegration.test.ts index 3b83726fca..12a6971857 100644 --- a/packages/core/test/integrations/logEnricherIntegration.test.ts +++ b/packages/core/test/integrations/logEnricherIntegration.test.ts @@ -1,5 +1,5 @@ import type { Client, Log } from '@sentry/core'; -import { debug } from '@sentry/core'; +import { debug, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { logEnricherIntegration } from '../../src/js/integrations/logEnricherIntegration'; import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; @@ -11,6 +11,9 @@ jest.mock('@sentry/core', () => ({ debug: { log: jest.fn(), }, + getCurrentScope: jest.fn(), + getGlobalScope: jest.fn(), + getIsolationScope: jest.fn(), })); const mockLogger = debug as jest.Mocked; @@ -49,6 +52,13 @@ describe('LogEnricher Integration', () => { } as unknown as jest.Mocked; (NATIVE as jest.Mocked).fetchNativeLogAttributes = mockFetchNativeLogAttributes; + + // Mock scope methods + (getCurrentScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ attributes: {} }), + }); + (getGlobalScope as jest.Mock).mockReturnValue({ getScopeData: jest.fn().mockReturnValue({ attributes: {} }) }); + (getIsolationScope as jest.Mock).mockReturnValue({ getScopeData: jest.fn().mockReturnValue({ attributes: {} }) }); }); afterEach(() => { @@ -516,4 +526,229 @@ describe('LogEnricher Integration', () => { expect(mockGetIntegrationByName).toHaveBeenCalledWith('MobileReplay'); }); }); + + describe('scope attributes', () => { + let logHandler: (log: Log) => void; + let mockLog: Log; + + beforeEach(async () => { + const integration = logEnricherIntegration(); + + const mockNativeResponse: NativeDeviceContextsResponse = { + contexts: { + device: { + brand: 'Apple', + model: 'iPhone 14', + } as Record, + }, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(mockNativeResponse); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + const beforeCaptureLogCall = mockOn.mock.calls.find(call => call[0] === 'beforeCaptureLog'); + expect(beforeCaptureLogCall).toBeDefined(); + logHandler = beforeCaptureLogCall[1]; + + mockLog = { + message: 'Test log message', + level: 'info', + attributes: {}, + }; + }); + + it('should apply attributes from global scope to logs', () => { + (getGlobalScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + is_admin: true, + auth_provider: 'google', + }, + }), + }); + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + is_admin: true, + auth_provider: 'google', + }); + }); + + it('should apply attributes from isolation scope to logs', () => { + (getIsolationScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + session_id: 'abc123', + user_tier: 'premium', + }, + }), + }); + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + session_id: 'abc123', + user_tier: 'premium', + }); + }); + + it('should apply attributes from current scope to logs', () => { + (getCurrentScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + step: 'authentication', + attempt: 1, + }, + }), + }); + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + step: 'authentication', + attempt: 1, + }); + }); + + it('should merge attributes from all scopes with correct precedence', () => { + (getGlobalScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + is_admin: true, + environment: 'production', + }, + }), + }); + + (getIsolationScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + environment: 'staging', + session_id: 'xyz789', + }, + }), + }); + + (getCurrentScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + environment: 'development', + step: 'login', + }, + }), + }); + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + is_admin: true, + environment: 'development', // Current scope wins + session_id: 'xyz789', + step: 'login', + }); + }); + + it('should only include string, number, and boolean attribute values', () => { + (getCurrentScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + stringAttr: 'value', + numberAttr: 42, + boolAttr: false, + objectAttr: { nested: 'object' }, // Should be filtered out + arrayAttr: [1, 2, 3], // Should be filtered out + nullAttr: null, // Should be filtered out + undefinedAttr: undefined, // Should be filtered out + }, + }), + }); + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + stringAttr: 'value', + numberAttr: 42, + boolAttr: false, + }); + expect(mockLog.attributes).not.toHaveProperty('objectAttr'); + expect(mockLog.attributes).not.toHaveProperty('arrayAttr'); + expect(mockLog.attributes).not.toHaveProperty('nullAttr'); + expect(mockLog.attributes).not.toHaveProperty('undefinedAttr'); + }); + + it('should not override existing log attributes with scope attributes', () => { + (getCurrentScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + step: 'authentication', + user_id: 'scope-user', + }, + }), + }); + + mockLog.attributes = { + user_id: 'log-user', // This should not be overridden + custom: 'value', + }; + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + user_id: 'log-user', // Original value preserved + custom: 'value', + step: 'authentication', + }); + }); + + it('should handle scopes without getScopeData method', () => { + (getCurrentScope as jest.Mock).mockReturnValue({}); + (getGlobalScope as jest.Mock).mockReturnValue({}); + (getIsolationScope as jest.Mock).mockReturnValue({}); + + logHandler(mockLog); + + // Should not throw and should still add device attributes + expect(mockLog.attributes).toMatchObject({ + 'device.brand': 'Apple', + 'device.model': 'iPhone 14', + }); + }); + + it('should handle null or undefined scopes', () => { + (getCurrentScope as jest.Mock).mockReturnValue(null); + (getGlobalScope as jest.Mock).mockReturnValue(undefined); + (getIsolationScope as jest.Mock).mockReturnValue(null); + + logHandler(mockLog); + + // Should not throw and should still add device attributes + expect(mockLog.attributes).toMatchObject({ + 'device.brand': 'Apple', + 'device.model': 'iPhone 14', + }); + }); + + it('should apply scope attributes before device attributes so they can be overridden', () => { + (getCurrentScope as jest.Mock).mockReturnValue({ + getScopeData: jest.fn().mockReturnValue({ + attributes: { + 'device.brand': 'CustomBrand', // Should be overridden by native + }, + }), + }); + + logHandler(mockLog); + + expect(mockLog.attributes).toMatchObject({ + 'device.brand': 'Apple', // Native value should override + 'device.model': 'iPhone 14', + }); + }); + }); }); diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts index 83d15681b5..89c7486575 100644 --- a/packages/core/test/mockWrapper.ts +++ b/packages/core/test/mockWrapper.ts @@ -41,6 +41,8 @@ const NATIVE: MockInterface = { setExtra: jest.fn(), setUser: jest.fn(), setTag: jest.fn(), + setAttribute: jest.fn(), + setAttributes: jest.fn(), nativeCrash: jest.fn(), @@ -92,6 +94,7 @@ NATIVE.crashedLastRun.mockResolvedValue(false); NATIVE.popTimeToDisplayFor.mockResolvedValue(null); NATIVE.getNewScreenTimeToDisplay.mockResolvedValue(null); NATIVE.primitiveProcessor.mockReturnValue(''); +NATIVE.fetchNativeLogAttributes = jest.fn().mockResolvedValue({}); export const getRNSentryModule = jest.fn(); export { NATIVE }; diff --git a/packages/core/test/scopeSync.test.ts b/packages/core/test/scopeSync.test.ts index abfba69291..43a8146648 100644 --- a/packages/core/test/scopeSync.test.ts +++ b/packages/core/test/scopeSync.test.ts @@ -116,6 +116,8 @@ describe('ScopeSync', () => { let setExtrasScopeSpy: jest.SpyInstance; let addBreadcrumbScopeSpy: jest.SpyInstance; let setContextScopeSpy: jest.SpyInstance; + let setAttributeScopeSpy: jest.SpyInstance; + let setAttributesScopeSpy: jest.SpyInstance; beforeAll(() => { const testScope = SentryCore.getIsolationScope(); @@ -126,6 +128,8 @@ describe('ScopeSync', () => { setExtrasScopeSpy = jest.spyOn(testScope, 'setExtras'); addBreadcrumbScopeSpy = jest.spyOn(testScope, 'addBreadcrumb'); setContextScopeSpy = jest.spyOn(testScope, 'setContext'); + setAttributeScopeSpy = jest.spyOn(testScope, 'setAttribute'); + setAttributesScopeSpy = jest.spyOn(testScope, 'setAttributes'); }); beforeEach(() => { @@ -214,5 +218,70 @@ describe('ScopeSync', () => { expect(NATIVE.setContext).toHaveBeenCalledExactlyOnceWith('key', { key: 'value' }); expect(setContextScopeSpy).toHaveBeenCalledExactlyOnceWith('key', { key: 'value' }); }); + + it('setAttribute', () => { + expect(SentryCore.getIsolationScope().setAttribute).not.toBe(setAttributeScopeSpy); + + SentryCore.getIsolationScope().setAttribute('session_id', 'abc123'); + expect(NATIVE.setAttribute).toHaveBeenCalledExactlyOnceWith('session_id', 'abc123'); + expect(setAttributeScopeSpy).toHaveBeenCalledExactlyOnceWith('session_id', 'abc123'); + }); + + it('setAttribute with number', () => { + SentryCore.getIsolationScope().setAttribute('request_count', 42); + expect(NATIVE.setAttribute).toHaveBeenCalledExactlyOnceWith('request_count', 42); + }); + + it('setAttribute with boolean', () => { + SentryCore.getIsolationScope().setAttribute('is_admin', true); + expect(NATIVE.setAttribute).toHaveBeenCalledExactlyOnceWith('is_admin', true); + }); + + it('setAttribute with non-primitive does not sync to native', () => { + SentryCore.getIsolationScope().setAttribute('complex', { nested: 'object' }); + expect(NATIVE.setAttribute).not.toHaveBeenCalled(); + }); + + it('setAttributes', () => { + expect(SentryCore.getIsolationScope().setAttributes).not.toBe(setAttributesScopeSpy); + + SentryCore.getIsolationScope().setAttributes({ + session_type: 'test', + request_count: 42, + is_admin: true, + }); + expect(NATIVE.setAttributes).toHaveBeenCalledExactlyOnceWith({ + session_type: 'test', + request_count: 42, + is_admin: true, + }); + expect(setAttributesScopeSpy).toHaveBeenCalledExactlyOnceWith({ + session_type: 'test', + request_count: 42, + is_admin: true, + }); + }); + + it('setAttributes filters non-primitive values', () => { + SentryCore.getIsolationScope().setAttributes({ + session_type: 'test', + request_count: 42, + complex: { nested: 'object' }, + is_admin: true, + }); + expect(NATIVE.setAttributes).toHaveBeenCalledExactlyOnceWith({ + session_type: 'test', + request_count: 42, + is_admin: true, + }); + }); + + it('setAttributes does not sync to native if all values are non-primitive', () => { + SentryCore.getIsolationScope().setAttributes({ + complex1: { nested: 'object' }, + complex2: ['array'], + }); + expect(NATIVE.setAttributes).not.toHaveBeenCalled(); + }); }); }); diff --git a/samples/expo/utils/setScopeProperties.ts b/samples/expo/utils/setScopeProperties.ts index 1fbfca4238..c698e6d40a 100644 --- a/samples/expo/utils/setScopeProperties.ts +++ b/samples/expo/utils/setScopeProperties.ts @@ -44,6 +44,18 @@ export const setScopeProperties = () => { undefinedTest: undefined, }); + // Set scope attributes that will be automatically applied to logs + Sentry.getGlobalScope().setAttributes({ + is_admin: true, + auth_provider: 'auth', + }); + + // Set scope attributes on the current scope + Sentry.getCurrentScope().setAttributes({ + session_type: 'test', + request_count: 1, + }); + Sentry.addBreadcrumb({ level: 'info' as SeverityLevel, message: `TEST-BREADCRUMB-INFO: ${dateString}`, diff --git a/samples/react-native-macos/src/setScopeProperties.ts b/samples/react-native-macos/src/setScopeProperties.ts index 1fbfca4238..a616f3e418 100644 --- a/samples/react-native-macos/src/setScopeProperties.ts +++ b/samples/react-native-macos/src/setScopeProperties.ts @@ -44,6 +44,18 @@ export const setScopeProperties = () => { undefinedTest: undefined, }); + // Set scope attributes that will be automatically applied to logs + Sentry.getGlobalScope().setAttributes({ + is_admin: true, + auth_provider: 'google', + }); + + // Set scope attributes on the current scope + Sentry.getCurrentScope().setAttributes({ + session_type: 'test', + request_count: 42, + }); + Sentry.addBreadcrumb({ level: 'info' as SeverityLevel, message: `TEST-BREADCRUMB-INFO: ${dateString}`, diff --git a/samples/react-native/src/setScopeProperties.ts b/samples/react-native/src/setScopeProperties.ts index 1fbfca4238..5693fc1776 100644 --- a/samples/react-native/src/setScopeProperties.ts +++ b/samples/react-native/src/setScopeProperties.ts @@ -44,6 +44,16 @@ export const setScopeProperties = () => { undefinedTest: undefined, }); + Sentry.getGlobalScope().setAttributes({ + is_admin: true, + auth_provider: 'auth', + }); + + Sentry.getCurrentScope().setAttributes({ + session_type: 'test', + request_count: 1, + }); + Sentry.addBreadcrumb({ level: 'info' as SeverityLevel, message: `TEST-BREADCRUMB-INFO: ${dateString}`,