Skip to content

Commit d63330b

Browse files
authored
Support for applying scope attributes to logs (#5579)
* Support for applying scope attributes to logs * Scope attributes sync (without native) * Sync scope attributes to native * setContext for android instead of attributes * Changelog entry * Changelog fix * Placeholders for the missing functions * Updated extractScopeAttributes function * Fix for logEnricherIntegration.ts * Maestro iOS version update * Version bump reverted
1 parent 400f0ac commit d63330b

File tree

15 files changed

+519
-4
lines changed

15 files changed

+519
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Features
1212

13+
- Add support for applying scope attributes to JS logs ([#5579](https://github.com/getsentry/sentry-react-native/pull/5579))
1314
- Add experimental `sentry-span-attributes` prop to attach custom attributes to user interaction spans ([#5569](https://github.com/getsentry/sentry-react-native/pull/5569))
1415
```tsx
1516
<Pressable

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,26 @@ public void setTag(String key, String value) {
933933
});
934934
}
935935

936+
public void setAttribute(String key, String value) {
937+
// TODO(alwx): This is not implemented in sentry-android yet
938+
/*
939+
* Sentry.configureScope(
940+
* scope -> {
941+
* scope.setAttribute(key, value);
942+
* });
943+
*/
944+
}
945+
946+
public void setAttributes(ReadableMap attributes) {
947+
// TODO(alwx): This is not implemented in sentry-android yet
948+
/*
949+
* Sentry.configureScope(
950+
* scope -> {
951+
* scope.setAttributes(attributes);
952+
* });
953+
*/
954+
}
955+
936956
public void closeNativeSdk(Promise promise) {
937957
Sentry.close();
938958

packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ public void setTag(String key, String value) {
112112
this.impl.setTag(key, value);
113113
}
114114

115+
@Override
116+
public void setAttribute(String key, String value) {
117+
this.impl.setAttribute(key, value);
118+
}
119+
120+
@Override
121+
public void setAttributes(ReadableMap attributes) {
122+
this.impl.setAttributes(attributes);
123+
}
124+
115125
@Override
116126
public void closeNativeSdk(Promise promise) {
117127
this.impl.closeNativeSdk(promise);

packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ public void setTag(String key, String value) {
112112
this.impl.setTag(key, value);
113113
}
114114

115+
@ReactMethod
116+
public void setAttribute(String key, String value) {
117+
this.impl.setAttribute(key, value);
118+
}
119+
120+
@ReactMethod
121+
public void setAttributes(ReadableMap attributes) {
122+
this.impl.setAttributes(attributes);
123+
}
124+
115125
@ReactMethod
116126
public void closeNativeSdk(Promise promise) {
117127
this.impl.closeNativeSdk(promise);
@@ -132,6 +142,11 @@ public void fetchNativeDeviceContexts(Promise promise) {
132142
this.impl.fetchNativeDeviceContexts(promise);
133143
}
134144

145+
@ReactMethod
146+
public void fetchNativeLogAttributes(Promise promise) {
147+
this.impl.fetchNativeLogAttributes(promise);
148+
}
149+
135150
@ReactMethod
136151
public void fetchNativeSdkInfo(Promise promise) {
137152
this.impl.fetchNativeSdkInfo(promise);

packages/core/ios/RNSentry.mm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,22 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
723723
configureScope:^(SentryScope *_Nonnull scope) { [scope setTagValue:value forKey:key]; }];
724724
}
725725

726+
RCT_EXPORT_METHOD(setAttribute : (NSString *)key value : (NSString *)value)
727+
{
728+
// TODO(alwx): This is not implemented in sentry-cocoa yet
729+
/*[SentrySDKWrapper
730+
configureScope:^(SentryScope *_Nonnull scope) { [scope setAttribute:value forKey:key]; }];*/
731+
}
732+
733+
RCT_EXPORT_METHOD(setAttributes : (NSDictionary *)attributes)
734+
{
735+
// TODO(alwx): This is not implemented in sentry-cocoa yet
736+
/*[SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
737+
[attributes enumerateKeysAndObjectsUsingBlock:^(
738+
NSString *key, NSString *value, BOOL *stop) { [scope setAttribute:value forKey:key]; }];
739+
}];*/
740+
}
741+
726742
RCT_EXPORT_METHOD(crash) { [SentrySDKWrapper crash]; }
727743

728744
RCT_EXPORT_METHOD(

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/integrations/logEnricherIntegration.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable complexity */
22
import type { Integration, Log } from '@sentry/core';
3-
import { debug } from '@sentry/core';
3+
import { debug, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core';
44
import type { ReactNativeClient } from '../client';
55
import { NATIVE } from '../wrapper';
66

@@ -33,7 +33,7 @@ let NativeCache: Record<string, unknown> | undefined = undefined;
3333
*
3434
* @param logAttributes - The log attributes object to modify.
3535
* @param key - The attribute key to set.
36-
* @param value - The value to set (only sets if truthy and key not present).
36+
* @param value - The value to set (only sets if not null/undefined and key not present).
3737
* @param setEvenIfPresent - Whether to set the attribute if it is present. Defaults to true.
3838
*/
3939
function setLogAttribute(
@@ -42,7 +42,7 @@ function setLogAttribute(
4242
value: unknown,
4343
setEvenIfPresent = true,
4444
): void {
45-
if (value && (!logAttributes[key] || setEvenIfPresent)) {
45+
if (value != null && (!logAttributes[key] || setEvenIfPresent)) {
4646
logAttributes[key] = value;
4747
}
4848
}
@@ -79,6 +79,13 @@ function processLog(log: Log, client: ReactNativeClient): void {
7979
// Save log.attributes to a new variable
8080
const logAttributes = log.attributes ?? {};
8181

82+
// Apply scope attributes from all active scopes (global, isolation, and current)
83+
// These are applied first so they can be overridden by more specific attributes
84+
const scopeAttributes = collectScopeAttributes();
85+
Object.keys(scopeAttributes).forEach((key: string) => {
86+
setLogAttribute(logAttributes, key, scopeAttributes[key], false);
87+
});
88+
8289
// Use setLogAttribute with the variable instead of direct assignment
8390
setLogAttribute(logAttributes, 'device.brand', NativeCache.brand);
8491
setLogAttribute(logAttributes, 'device.model', NativeCache.model);
@@ -93,3 +100,43 @@ function processLog(log: Log, client: ReactNativeClient): void {
93100
// Set log.attributes to the variable
94101
log.attributes = logAttributes;
95102
}
103+
104+
/**
105+
* Extracts primitive attributes from a scope and merges them into the target object.
106+
* Only string, number, and boolean attribute values are included.
107+
*
108+
* @param scope - The scope to extract attributes from
109+
* @param target - The target object to merge attributes into
110+
*/
111+
function extractScopeAttributes(
112+
scope: ReturnType<typeof getCurrentScope>,
113+
target: Record<string, string | number | boolean>,
114+
): void {
115+
if (scope && typeof scope.getScopeData === 'function') {
116+
const scopeAttrs = scope?.getScopeData?.().attributes || {};
117+
Object.keys(scopeAttrs).forEach(key => {
118+
const value = scopeAttrs[key];
119+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
120+
target[key] = value;
121+
}
122+
});
123+
}
124+
}
125+
126+
/**
127+
* Collects attributes from all active scopes (global, isolation, and current).
128+
* Only string, number, and boolean attribute values are supported.
129+
* Attributes are merged in order of precedence: global < isolation < current.
130+
*
131+
* @returns A merged object containing all scope attributes.
132+
*/
133+
function collectScopeAttributes(): Record<string, string | number | boolean> {
134+
const attributes: Record<string, string | number | boolean> = {};
135+
136+
// Collect attributes from all scopes in order of precedence
137+
extractScopeAttributes(getGlobalScope(), attributes);
138+
extractScopeAttributes(getIsolationScope(), attributes);
139+
extractScopeAttributes(getCurrentScope(), attributes);
140+
141+
return attributes;
142+
}

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
*/

0 commit comments

Comments
 (0)