Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Pressable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,26 @@ 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);
* });
*/
}

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();

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);
}

@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
16 changes: 16 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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(
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
52 changes: 49 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)) {
Copy link

Choose a reason for hiding this comment

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

Bug: The check !logAttributes[key] incorrectly uses truthiness instead of key presence, causing existing falsy attribute values (0, false, "") to be overridden when they shouldn't be.
Severity: MEDIUM

Suggested Fix

Replace the truthy check !logAttributes[key] with a proper key presence check, such as !(key in logAttributes). This will correctly verify if the key exists in the logAttributes object, regardless of its value's truthiness.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/core/src/js/integrations/logEnricherIntegration.ts#L45

Potential issue: In the `logEnricherIntegration`, when applying scope attributes to a
log event with `setEvenIfPresent` as `false`, the code uses a truthy check
(`!logAttributes[key]`) to determine if an attribute already exists. This causes
existing log attributes with falsy values (e.g., `count: 0`, `enabled: false`, or an
empty string) to be incorrectly overridden by attributes from the scope, even though the
intent is to preserve them. This leads to data loss in logs when these common falsy
values are used.

Did we get this right? 👍 / 👎 to inform future reviews.

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
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
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
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
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
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,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<typeof getCurrentScope>,
target: Record<string, string | number | boolean>,
): 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<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