Skip to content

Commit 63313a6

Browse files
Logs: Capture more device attributes (#5032)
* add javascript logic (WIP when to spawn cache) / implemented Android context info * fix logic and also implement ios side * remove setTimeout and use afterInit hook, add tests * yarn fix * use java 8 syntax * changelog * clang fix?
1 parent f43169e commit 63313a6

File tree

10 files changed

+603
-0
lines changed

10 files changed

+603
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
## Features
12+
13+
- Logs now contains more attributes like release, os and device information ([#5032](https://github.com/getsentry/sentry-react-native/pull/5032))
14+
1115
### Dependencies
1216

1317
- Bump Android SDK from v8.17.0 to v8.18.0 ([#5034](https://github.com/getsentry/sentry-react-native/pull/5034))

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,13 @@ private String readStringFromFile(File path) throws IOException {
988988
}
989989
}
990990

991+
public void fetchNativeLogAttributes(Promise promise) {
992+
final @NotNull SentryOptions options = ScopesAdapter.getInstance().getOptions();
993+
final @Nullable Context context = this.getReactApplicationContext().getApplicationContext();
994+
final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope();
995+
fetchNativeLogContexts(promise, options, context, currentScope);
996+
}
997+
991998
public void fetchNativeDeviceContexts(Promise promise) {
992999
final @NotNull SentryOptions options = ScopesAdapter.getInstance().getOptions();
9931000
final @Nullable Context context = this.getReactApplicationContext().getApplicationContext();
@@ -1025,6 +1032,47 @@ protected void fetchNativeDeviceContexts(
10251032
promise.resolve(deviceContext);
10261033
}
10271034

1035+
// Basically fetchNativeDeviceContexts but filtered to only get contexts info.
1036+
protected void fetchNativeLogContexts(
1037+
Promise promise,
1038+
final @NotNull SentryOptions options,
1039+
final @Nullable Context osContext,
1040+
final @Nullable IScope currentScope) {
1041+
if (!(options instanceof SentryAndroidOptions) || osContext == null) {
1042+
promise.resolve(null);
1043+
return;
1044+
}
1045+
1046+
Object contextsObj =
1047+
InternalSentrySdk.serializeScope(osContext, (SentryAndroidOptions) options, currentScope)
1048+
.get("contexts");
1049+
1050+
if (!(contextsObj instanceof Map)) {
1051+
promise.resolve(null);
1052+
return;
1053+
}
1054+
1055+
@SuppressWarnings("unchecked")
1056+
Map<String, Object> contextsMap = (Map<String, Object>) contextsObj;
1057+
1058+
Map<String, Object> contextItems = new HashMap<>();
1059+
if (contextsMap.containsKey("os")) {
1060+
contextItems.put("os", contextsMap.get("os"));
1061+
}
1062+
1063+
if (contextsMap.containsKey("device")) {
1064+
contextItems.put("device", contextsMap.get("device"));
1065+
}
1066+
1067+
contextItems.put("release", options.getRelease());
1068+
1069+
Map<String, Object> logContext = new HashMap<>();
1070+
logContext.put("contexts", contextItems);
1071+
Object filteredContext = RNSentryMapConverter.convertToWritable(logContext);
1072+
1073+
promise.resolve(filteredContext);
1074+
}
1075+
10281076
public void fetchNativeSdkInfo(Promise promise) {
10291077
final @Nullable SdkVersion sdkVersion =
10301078
ScopesAdapter.getInstance().getOptions().getSdkVersion();

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ public void disableNativeFramesTracking() {
127127
this.impl.disableNativeFramesTracking();
128128
}
129129

130+
@Override
131+
public void fetchNativeLogAttributes(Promise promise) {
132+
this.impl.fetchNativeLogAttributes(promise);
133+
}
134+
130135
@Override
131136
public void fetchNativeDeviceContexts(Promise promise) {
132137
this.impl.fetchNativeDeviceContexts(promise);

packages/core/ios/RNSentry.mm

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,58 @@ - (NSDictionary *)fetchNativeStackFramesBy:(NSArray<NSNumber *> *)instructionsAd
523523
return [self fetchNativeStackFramesBy:instructionsAddr symbolicate:dladdr];
524524
}
525525

526+
RCT_EXPORT_METHOD(fetchNativeLogAttributes
527+
: (RCTPromiseResolveBlock)resolve rejecter
528+
: (RCTPromiseRejectBlock)reject)
529+
{
530+
__block NSMutableDictionary<NSString *, id> *result = [NSMutableDictionary new];
531+
532+
[SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) {
533+
// Serialize to get contexts dictionary
534+
NSDictionary *serializedScope = [scope serialize];
535+
NSDictionary *allContexts = serializedScope[@"context"]; // It's singular here, annoyingly
536+
537+
NSMutableDictionary *contexts = [NSMutableDictionary new];
538+
539+
NSDictionary *device = allContexts[@"device"];
540+
if ([device isKindOfClass:[NSDictionary class]]) {
541+
contexts[@"device"] = device;
542+
}
543+
544+
NSDictionary *os = allContexts[@"os"];
545+
if ([os isKindOfClass:[NSDictionary class]]) {
546+
contexts[@"os"] = os;
547+
}
548+
549+
NSString *releaseName = [SentrySDK options].releaseName;
550+
if (releaseName) {
551+
contexts[@"release"] = releaseName;
552+
}
553+
// Merge extra context
554+
NSDictionary *extraContext = [PrivateSentrySDKOnly getExtraContext];
555+
556+
if (extraContext) {
557+
NSDictionary *extraDevice = extraContext[@"device"];
558+
if ([extraDevice isKindOfClass:[NSDictionary class]]) {
559+
NSMutableDictionary *mergedDevice =
560+
[contexts[@"device"] mutableCopy] ?: [NSMutableDictionary new];
561+
[mergedDevice addEntriesFromDictionary:extraDevice];
562+
contexts[@"device"] = mergedDevice;
563+
}
564+
565+
NSDictionary *extraOS = extraContext[@"os"];
566+
if ([extraOS isKindOfClass:[NSDictionary class]]) {
567+
NSMutableDictionary *mergedOS =
568+
[contexts[@"os"] mutableCopy] ?: [NSMutableDictionary new];
569+
[mergedOS addEntriesFromDictionary:extraOS];
570+
contexts[@"os"] = mergedOS;
571+
}
572+
}
573+
result[@"contexts"] = contexts;
574+
}];
575+
resolve(result);
576+
}
577+
526578
RCT_EXPORT_METHOD(fetchNativeDeviceContexts
527579
: (RCTPromiseResolveBlock)resolve rejecter
528580
: (RCTPromiseRejectBlock)reject)

packages/core/src/js/NativeRNSentry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Spec extends TurboModule {
2424
fetchNativeRelease(): Promise<NativeReleaseResponse>;
2525
fetchNativeSdkInfo(): Promise<Package | null>;
2626
fetchNativeDeviceContexts(): Promise<NativeDeviceContextsResponse | null>;
27+
fetchNativeLogAttributes(): Promise<NativeDeviceContextsResponse | null>;
2728
fetchNativeAppStart(): Promise<NativeAppStartResponse | null>;
2829
fetchNativeFrames(): Promise<NativeFramesResponse | null>;
2930
initNativeSdk(options: UnsafeObject): Promise<boolean>;

packages/core/src/js/integrations/default.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
httpClientIntegration,
2424
httpContextIntegration,
2525
inboundFiltersIntegration,
26+
logEnricherIntegration,
2627
mobileReplayIntegration,
2728
modulesLoaderIntegration,
2829
nativeLinkedErrorsIntegration,
@@ -84,6 +85,9 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
8485
if (options.enableNative) {
8586
integrations.push(deviceContextIntegration());
8687
integrations.push(modulesLoaderIntegration());
88+
if (options._experiments?.enableLogs) {
89+
integrations.push(logEnricherIntegration());
90+
}
8791
if (options.attachScreenshot) {
8892
integrations.push(screenshotIntegration());
8993
}

packages/core/src/js/integrations/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export { appRegistryIntegration } from './appRegistry';
2424
export { timeToDisplayIntegration } from '../tracing/integrations/timeToDisplayIntegration';
2525
export { breadcrumbsIntegration } from './breadcrumbs';
2626
export { primitiveTagIntegration } from './primitiveTagIntegration';
27+
export { logEnricherIntegration } from './logEnricherIntegration';
2728

2829
export {
2930
browserApiErrorsIntegration,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable complexity */
2+
import type { Integration, Log } from '@sentry/core';
3+
import { logger } from '@sentry/core';
4+
import type { ReactNativeClient } from '../client';
5+
import { NATIVE } from '../wrapper';
6+
7+
const INTEGRATION_NAME = 'LogEnricher';
8+
9+
export const logEnricherIntegration = (): Integration => {
10+
return {
11+
name: INTEGRATION_NAME,
12+
setup(client: ReactNativeClient) {
13+
client.on('afterInit', () => {
14+
cacheLogContext().then(
15+
() => {
16+
client.on('beforeCaptureLog', (log: Log) => {
17+
processLog(log);
18+
});
19+
},
20+
reason => {
21+
logger.log(reason);
22+
},
23+
);
24+
});
25+
},
26+
};
27+
};
28+
29+
let NativeCache: Record<string, unknown> | undefined = undefined;
30+
31+
async function cacheLogContext(): Promise<void> {
32+
try {
33+
const response = await NATIVE.fetchNativeLogAttributes();
34+
35+
NativeCache = {
36+
...(response?.contexts?.device && {
37+
brand: response.contexts.device?.brand,
38+
model: response.contexts.device?.model,
39+
family: response.contexts.device?.family,
40+
}),
41+
...(response?.contexts?.os && {
42+
os: response.contexts.os.name,
43+
version: response.contexts.os.version,
44+
}),
45+
...(response?.contexts?.release && {
46+
release: response.contexts.release,
47+
}),
48+
};
49+
} catch (e) {
50+
return Promise.reject(`[LOGS]: Failed to prepare attributes from Native Layer: ${e}`);
51+
}
52+
return Promise.resolve();
53+
}
54+
55+
function processLog(log: Log): void {
56+
if (NativeCache === undefined) {
57+
return;
58+
}
59+
60+
log.attributes = log.attributes ?? {};
61+
NativeCache.brand && (log.attributes['device.brand'] = NativeCache.brand);
62+
NativeCache.model && (log.attributes['device.model'] = NativeCache.model);
63+
NativeCache.family && (log.attributes['device.family'] = NativeCache.family);
64+
NativeCache.os && (log.attributes['os.name'] = NativeCache.os);
65+
NativeCache.version && (log.attributes['os.version'] = NativeCache.version);
66+
NativeCache.release && (log.attributes['sentry.release'] = NativeCache.release);
67+
}

packages/core/src/js/wrapper.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ interface SentryNativeWrapper {
8484

8585
fetchNativeRelease(): PromiseLike<NativeReleaseResponse>;
8686
fetchNativeDeviceContexts(): PromiseLike<NativeDeviceContextsResponse | null>;
87+
fetchNativeLogAttributes(): Promise<NativeDeviceContextsResponse | null>;
8788
fetchNativeAppStart(): PromiseLike<NativeAppStartResponse | null>;
8889
fetchNativeFrames(): PromiseLike<NativeFramesResponse | null>;
8990
fetchNativeSdkInfo(): PromiseLike<Package | null>;
@@ -282,6 +283,19 @@ export const NATIVE: SentryNativeWrapper = {
282283
return nativeIsReady;
283284
},
284285

286+
/**
287+
* Fetches the attributes to be set into logs from Native
288+
*/
289+
async fetchNativeLogAttributes(): Promise<NativeDeviceContextsResponse | null> {
290+
if (!this.enableNative) {
291+
throw this._DisabledNativeError;
292+
}
293+
if (!this._isModuleLoaded(RNSentry)) {
294+
throw this._NativeClientError;
295+
}
296+
297+
return RNSentry.fetchNativeLogAttributes();
298+
},
285299
/**
286300
* Fetches the release from native
287301
*/

0 commit comments

Comments
 (0)