Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

## Unreleased

### Features

Comment thread
alwx marked this conversation as resolved.
- Add support for applying scope attributes to logs ([#5579](https://github.com/getsentry/sentry-react-native/pull/5579))
Comment thread
alwx marked this conversation as resolved.
Outdated
- 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
<Pressable
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,20 @@
configureScope:^(SentryScope *_Nonnull scope) { [scope setTagValue:value forKey:key]; }];
}

RCT_EXPORT_METHOD(setAttribute : (NSString *)key value : (NSString *)value)
{
[SentrySDKWrapper
configureScope:^(SentryScope *_Nonnull scope) { [scope setAttribute:value forKey:key]; }];

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy macos production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy macos dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build new ios production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios dev dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build new ios dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios production dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios production dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios dev dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 729 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'
Comment thread
antonis marked this conversation as resolved.
Outdated
}

RCT_EXPORT_METHOD(setAttributes : (NSDictionary *)attributes)
{
[SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
[attributes enumerateKeysAndObjectsUsingBlock:^(
NSString *key, NSString *value, BOOL *stop) { [scope setAttribute:value forKey:key]; }];

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy macos production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy macos dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build new ios production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios dev dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build new ios dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios production dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios production dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build legacy ios dev dynamic-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios dev no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'

Check failure on line 736 in packages/core/ios/RNSentry.mm

View workflow job for this annotation

GitHub Actions / Build ios production no-frameworks

no visible @interface for 'SentryScope' declares the selector 'setAttribute:forKey:'
}];
}

RCT_EXPORT_METHOD(crash) { [SentrySDKWrapper crash]; }

RCT_EXPORT_METHOD(
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined | null>;
fetchViewHierarchy(): Promise<number[] | undefined | null>;
Expand Down
54 changes: 51 additions & 3 deletions packages/core/src/js/integrations/logEnricherIntegration.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -33,7 +33,7 @@ let NativeCache: Record<string, unknown> | 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(
Expand All @@ -42,7 +42,7 @@ function setLogAttribute(
value: unknown,
setEvenIfPresent = true,
): void {
if (value && (!logAttributes[key] || setEvenIfPresent)) {
if (value != null && (!logAttributes[key] || setEvenIfPresent)) {
Comment thread
sentry[bot] marked this conversation as resolved.
logAttributes[key] = value;
}
}
Expand Down Expand Up @@ -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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we try using

  const {
    user: { id, email, username },
    attributes: scopeAttributes = {},
  } = getCombinedScopeData(getIsolationScope(), currentScope);

I am not sure why Sentry JS Opted to only load data from isolationScope and currentScope, might be worth checking why.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good suggestion and my first try as well but the thing is getCombinedScopeData is an internal function and not getting exported by the JS SDK. That's why I do the merging and use getCombinedScopeAttributes here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the global scope: they do use it, they just call getGlobalScope() directly here: https://github.com/getsentry/sentry-javascript/blob/develop/packages/core/src/utils/scopeData.ts#L125

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh True, I did a quick test on capacitor and I was able to import it from @sentry/core but not on React Native

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: it should be be exported on @sentry/core

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);
Expand All @@ -93,3 +100,44 @@ 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<typeof getCurrentScope>,
target: Record<string, string | number | boolean>,
): void {
if (scope && typeof scope.getScopeData === 'function') {
const scopeData = scope.getScopeData();
const scopeAttrs = scopeData.attributes || {};
Object.keys(scopeAttrs).forEach((key: string) => {
const value = scopeAttrs[key];
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
target[key] = value;
}
});
}
Comment thread
alwx marked this conversation as resolved.
}

/**
* 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<string, string | number | boolean> {
const attributes: Record<string, string | number | boolean> = {};

// Collect attributes from all scopes in order of precedence
extractScopeAttributes(getGlobalScope(), attributes);
extractScopeAttributes(getIsolationScope(), attributes);
extractScopeAttributes(getCurrentScope(), attributes);

return attributes;
}
24 changes: 24 additions & 0 deletions packages/core/src/js/scopeSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>): Scope => {
// Filter to only primitive types
const primitiveAttrs: Record<string, string | number | boolean> = {};
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);
});
}
39 changes: 39 additions & 0 deletions packages/core/src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | number | boolean>): void;

nativeCrash(): void;

Expand Down Expand Up @@ -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<string, string | number | boolean>): void {
if (!this.enableNative) {
return;
}
if (!this._isModuleLoaded(RNSentry)) {
throw this._NativeClientError;
}

const serializedAttributes: Record<string, string> = {};
Object.keys(attributes).forEach(key => {
serializedAttributes[key] = this.primitiveProcessor(attributes[key]);
});

RNSentry.setAttributes(serializedAttributes);
},

/**
* Closes the Native Layer SDK
*/
Expand Down
Loading
Loading