Skip to content

Commit c022e6f

Browse files
feat(contexts): Sync native iOS contexts to JS (#2713)
1 parent be4a808 commit c022e6f

File tree

11 files changed

+402
-41
lines changed

11 files changed

+402
-41
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. ([#2713](https://github.com/getsentry/sentry-react-native/pull/2713))
8+
- `breadcrumb.level` value `log` is transformed to `debug` when syncing with native layers.
9+
- Remove `breadcrumb.level` value `critical` transformation to `fatal`.
10+
- Default `breadcrumb.level` is `info`
11+
312
## 5.0.0-alpha.11
413

514
- Latest changes from 4.13.0

ios/RNSentry.mm

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -175,33 +175,27 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
175175
rejecter:(RCTPromiseRejectBlock)reject)
176176
{
177177
NSLog(@"Bridge call to: deviceContexts");
178-
NSMutableDictionary<NSString *, id> *contexts = [NSMutableDictionary new];
178+
__block NSMutableDictionary<NSString *, id> *contexts;
179179
// Temp work around until sorted out this API in sentry-cocoa.
180180
// TODO: If the callback isnt' executed the promise wouldn't be resolved.
181181
[SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
182-
NSDictionary<NSString *, id> *serializedScope = [scope serialize];
183-
// Scope serializes as 'context' instead of 'contexts' as it does for the event.
184-
NSDictionary<NSString *, id> *tempContexts = [serializedScope valueForKey:@"context"];
185-
186-
NSMutableDictionary<NSString *, id> *user = [NSMutableDictionary new];
187-
188-
NSDictionary<NSString *, id> *tempUser = [serializedScope valueForKey:@"user"];
189-
if (tempUser != nil) {
190-
[user addEntriesFromDictionary:[tempUser valueForKey:@"user"]];
191-
} else {
192-
[user setValue:PrivateSentrySDKOnly.installationID forKey:@"id"];
182+
NSDictionary<NSString *, id> *serializedScope = [scope serialize];
183+
contexts = [serializedScope mutableCopy];
184+
185+
NSDictionary<NSString *, id> *user = [contexts valueForKey:@"user"];
186+
if (user == nil) {
187+
[contexts
188+
setValue:@{ @"id": PrivateSentrySDKOnly.installationID }
189+
forKey:@"user"];
193190
}
194-
[contexts setValue:user forKey:@"user"];
195191

196-
if (tempContexts != nil) {
197-
[contexts setValue:tempContexts forKey:@"context"];
198-
}
199192
if (PrivateSentrySDKOnly.options.debug) {
200193
NSData *data = [NSJSONSerialization dataWithJSONObject:contexts options:0 error:nil];
201194
NSString *debugContext = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
202195
NSLog(@"Contexts: %@", debugContext);
203196
}
204197
}];
198+
205199
resolve(contexts);
206200
}
207201

@@ -373,12 +367,12 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
373367
sentryLevel = kSentryLevelFatal;
374368
} else if ([levelString isEqualToString:@"warning"]) {
375369
sentryLevel = kSentryLevelWarning;
376-
} else if ([levelString isEqualToString:@"info"]) {
377-
sentryLevel = kSentryLevelInfo;
370+
} else if ([levelString isEqualToString:@"error"]) {
371+
sentryLevel = kSentryLevelError;
378372
} else if ([levelString isEqualToString:@"debug"]) {
379373
sentryLevel = kSentryLevelDebug;
380374
} else {
381-
sentryLevel = kSentryLevelError;
375+
sentryLevel = kSentryLevelInfo;
382376
}
383377
[breadcrumbInstance setLevel:sentryLevel];
384378

src/js/NativeRNSentry.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,35 @@ export type NativeReleaseResponse = {
5252
version: string;
5353
};
5454

55+
/**
56+
* This type describes serialized scope from sentry-cocoa. (This is not used for Android)
57+
* https://github.com/getsentry/sentry-cocoa/blob/master/Sources/Sentry/SentryScope.m
58+
*/
5559
export type NativeDeviceContextsResponse = {
56-
[key: string]: Record<string, unknown>;
60+
[key: string]: unknown;
61+
tags?: Record<string, string>;
62+
extra?: Record<string, unknown>;
63+
context?: Record<string, Record<string, unknown>>;
64+
user?: {
65+
userId?: string;
66+
email?: string;
67+
username?: string;
68+
ipAddress?: string;
69+
segment?: string;
70+
data?: Record<string, unknown>;
71+
};
72+
dist?: string;
73+
environment?: string;
74+
fingerprint?: string[];
75+
level?: string;
76+
breadcrumbs?: {
77+
level?: string;
78+
timestamp?: string;
79+
category?: string;
80+
type?: string;
81+
message?: string;
82+
data?: Record<string, unknown>;
83+
}[];
5784
};
5885

5986
export type NativeScreenshot = {

src/js/breadcrumb.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Breadcrumb, SeverityLevel } from '@sentry/types';
2+
import { severityLevelFromString } from '@sentry/utils';
3+
4+
export const DEFAULT_BREADCRUMB_LEVEL: SeverityLevel = 'info';
5+
6+
type BreadcrumbCandidate = {
7+
[K in keyof Partial<Breadcrumb>]: unknown;
8+
}
9+
10+
/**
11+
* Convert plain object to a valid Breadcrumb
12+
*/
13+
export function breadcrumbFromObject(candidate: BreadcrumbCandidate): Breadcrumb {
14+
const breadcrumb: Breadcrumb = {};
15+
16+
if (typeof candidate.type === 'string') {
17+
breadcrumb.type = candidate.type;
18+
}
19+
if (typeof candidate.level === 'string') {
20+
breadcrumb.level = severityLevelFromString(candidate.level);
21+
}
22+
if (typeof candidate.event_id === 'string') {
23+
breadcrumb.event_id = candidate.event_id;
24+
}
25+
if (typeof candidate.category === 'string') {
26+
breadcrumb.category = candidate.category;
27+
}
28+
if (typeof candidate.message === 'string') {
29+
breadcrumb.message = candidate.message;
30+
}
31+
if (typeof candidate.data === 'object' && candidate.data !== null) {
32+
breadcrumb.data = candidate.data;
33+
}
34+
if (typeof candidate.timestamp === 'string') {
35+
const timestamp = Date.parse(candidate.timestamp)
36+
if (!isNaN(timestamp)) {
37+
breadcrumb.timestamp = timestamp;
38+
}
39+
}
40+
41+
return breadcrumb;
42+
}

src/js/integrations/devicecontext.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
2-
import { Contexts, Event, Integration } from '@sentry/types';
3-
import { logger } from '@sentry/utils';
1+
/* eslint-disable complexity */
2+
import { Event, EventProcessor, Hub, Integration } from '@sentry/types';
3+
import { logger, severityLevelFromString } from '@sentry/utils';
44

5+
import { breadcrumbFromObject } from '../breadcrumb';
6+
import { NativeDeviceContextsResponse } from '../NativeRNSentry';
57
import { NATIVE } from '../wrapper';
68

79
/** Load device context from native. */
@@ -19,26 +21,71 @@ export class DeviceContext implements Integration {
1921
/**
2022
* @inheritDoc
2123
*/
22-
public setupOnce(): void {
24+
public setupOnce(
25+
addGlobalEventProcessor: (callback: EventProcessor) => void,
26+
getCurrentHub: () => Hub,
27+
): void {
2328
addGlobalEventProcessor(async (event: Event) => {
2429
const self = getCurrentHub().getIntegration(DeviceContext);
2530
if (!self) {
2631
return event;
2732
}
2833

34+
let native: NativeDeviceContextsResponse | null = null;
2935
try {
30-
const contexts = await NATIVE.fetchNativeDeviceContexts();
36+
native = await NATIVE.fetchNativeDeviceContexts();
37+
} catch (e) {
38+
logger.log(`Failed to get device context from native: ${e}`);
39+
}
40+
41+
if (!native) {
42+
return event;
43+
}
3144

32-
const context = contexts['context'] as Contexts ?? {};
33-
const user = contexts['user'] ?? {};
45+
const nativeUser = native.user;
46+
if (!event.user && nativeUser) {
47+
event.user = nativeUser;
48+
}
3449

35-
event.contexts = { ...context, ...event.contexts };
50+
const nativeContext = native.context;
51+
if (nativeContext) {
52+
event.contexts = { ...nativeContext, ...event.contexts };
53+
}
3654

37-
if (!event.user) {
38-
event.user = { ...user };
39-
}
40-
} catch (e) {
41-
logger.log(`Failed to get device context from native: ${e}`);
55+
const nativeTags = native.tags;
56+
if (nativeTags) {
57+
event.tags = { ...nativeTags, ...event.tags };
58+
}
59+
60+
const nativeExtra = native.extra;
61+
if (nativeExtra) {
62+
event.extra = { ...nativeExtra, ...event.extra };
63+
}
64+
65+
const nativeFingerprint = native.fingerprint;
66+
if (nativeFingerprint) {
67+
event.fingerprint = (event.fingerprint ?? []).concat(
68+
nativeFingerprint.filter((item) => (event.fingerprint ?? []).indexOf(item) < 0),
69+
)
70+
}
71+
72+
const nativeLevel = typeof native['level'] === 'string'
73+
? severityLevelFromString(native['level'])
74+
: undefined;
75+
if (!event.level && nativeLevel) {
76+
event.level = nativeLevel;
77+
}
78+
79+
const nativeEnvironment = native['environment'];
80+
if (!event.environment && nativeEnvironment) {
81+
event.environment = nativeEnvironment;
82+
}
83+
84+
const nativeBreadcrumbs = Array.isArray(native['breadcrumbs'])
85+
? native['breadcrumbs'].map(breadcrumbFromObject)
86+
: undefined;
87+
if (nativeBreadcrumbs) {
88+
event.breadcrumbs = nativeBreadcrumbs;
4289
}
4390

4491
return event;

src/js/scope.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Scope } from '@sentry/core';
22
import { Attachment, Breadcrumb, User } from '@sentry/types';
33

4+
import { DEFAULT_BREADCRUMB_LEVEL } from './breadcrumb';
45
import { NATIVE } from './wrapper';
56

67
/**
@@ -58,8 +59,12 @@ export class ReactNativeScope extends Scope {
5859
* @inheritDoc
5960
*/
6061
public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this {
61-
NATIVE.addBreadcrumb(breadcrumb);
62-
return super.addBreadcrumb(breadcrumb, maxBreadcrumbs);
62+
const mergedBreadcrumb = {
63+
...breadcrumb,
64+
level: breadcrumb.level || DEFAULT_BREADCRUMB_LEVEL,
65+
};
66+
NATIVE.addBreadcrumb(mergedBreadcrumb);
67+
return super.addBreadcrumb(mergedBreadcrumb, maxBreadcrumbs);
6368
}
6469

6570
/**

src/js/wrapper.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -566,11 +566,6 @@ export const NATIVE: SentryNativeWrapper = {
566566
if (level == 'log' as SeverityLevel) {
567567
return 'debug' as SeverityLevel;
568568
}
569-
else if (level == 'critical' as SeverityLevel) {
570-
return 'fatal' as SeverityLevel;
571-
}
572-
573-
574569
return level;
575570
},
576571

test/breadcrumb.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Breadcrumb } from '@sentry/types';
2+
3+
import { breadcrumbFromObject } from '../src/js/breadcrumb';
4+
5+
describe('Breadcrumb', () => {
6+
describe('breadcrumbFromObject', () => {
7+
it('convert a plain object to a valid Breadcrumb', () => {
8+
const candidate = {
9+
type: 'test',
10+
level: 'info',
11+
event_id: '1234',
12+
category: 'test',
13+
message: 'test',
14+
data: {
15+
test: 'test',
16+
},
17+
timestamp: '2020-01-01T00:00:00.000Z',
18+
};
19+
const breadcrumb = breadcrumbFromObject(candidate);
20+
expect(breadcrumb).toEqual(<Breadcrumb>{
21+
type: 'test',
22+
level: 'info',
23+
event_id: '1234',
24+
category: 'test',
25+
message: 'test',
26+
data: {
27+
test: 'test',
28+
},
29+
timestamp: 1577836800000,
30+
});
31+
});
32+
33+
it('convert plain object with invalid timestamp to a valid Breadcrumb', () => {
34+
const candidate = {
35+
type: 'test',
36+
level: 'info',
37+
timestamp: 'invalid',
38+
};
39+
const breadcrumb = breadcrumbFromObject(candidate);
40+
expect(breadcrumb).toEqual(<Breadcrumb>{
41+
type: 'test',
42+
level: 'info',
43+
});
44+
});
45+
46+
it('convert empty object to a valid Breadcrumb', () => {
47+
const candidate = {};
48+
const breadcrumb = breadcrumbFromObject(candidate);
49+
expect(breadcrumb).toEqual(<Breadcrumb>{});
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)