Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contexts): Sync native iOS contexts to JS #2713

Merged
merged 20 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

- Latest changes from 4.12.0

### Features

- Sync `tags`, `extra`, `fingerprint`, `level`, `environment` and `breadcrumbs` from `sentry-cocoa` during event processing. ([#2713](https://github.com/getsentry/sentry-react-native/pull/2713))
- `breadcrumb.level` value `log` is transformed to `debug` when syncing with native layers.
- Deprecated `breadcrumb.level` value `critical` is removed and not transformed.
- Default `breadcrumb.level` is `error`

### Breaking changes

- Message event current stack trace moved from exception to threads ([#2694](https://github.com/getsentry/sentry-react-native/pull/2694))
Expand Down
8 changes: 4 additions & 4 deletions android/src/main/java/io/sentry/react/RNSentryModuleImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,13 @@ public void addBreadcrumb(final ReadableMap breadcrumb) {
case "debug":
breadcrumbInstance.setLevel(SentryLevel.DEBUG);
break;
case "error":
breadcrumbInstance.setLevel(SentryLevel.ERROR);
break;
case "info":
default:
breadcrumbInstance.setLevel(SentryLevel.INFO);
break;
case "error":
default:
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
breadcrumbInstance.setLevel(SentryLevel.ERROR);
break;
}
}

Expand Down
26 changes: 10 additions & 16 deletions ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -175,33 +175,27 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event
rejecter:(RCTPromiseRejectBlock)reject)
{
NSLog(@"Bridge call to: deviceContexts");
NSMutableDictionary<NSString *, id> *contexts = [NSMutableDictionary new];
__block NSMutableDictionary<NSString *, id> *contexts;
// Temp work around until sorted out this API in sentry-cocoa.
// TODO: If the callback isnt' executed the promise wouldn't be resolved.
[SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
NSDictionary<NSString *, id> *serializedScope = [scope serialize];
// Scope serializes as 'context' instead of 'contexts' as it does for the event.
NSDictionary<NSString *, id> *tempContexts = [serializedScope valueForKey:@"context"];

NSMutableDictionary<NSString *, id> *user = [NSMutableDictionary new];

NSDictionary<NSString *, id> *tempUser = [serializedScope valueForKey:@"user"];
if (tempUser != nil) {
[user addEntriesFromDictionary:[tempUser valueForKey:@"user"]];
} else {
[user setValue:PrivateSentrySDKOnly.installationID forKey:@"id"];
NSDictionary<NSString *, id> *serializedScope = [scope serialize];
contexts = [serializedScope mutableCopy];

NSDictionary<NSString *, id> *user = [contexts valueForKey:@"user"];
if (user == nil) {
[contexts
setValue:@{ @"id": PrivateSentrySDKOnly.installationID }
forKey:@"user"];
}
[contexts setValue:user forKey:@"user"];

if (tempContexts != nil) {
[contexts setValue:tempContexts forKey:@"context"];
}
if (PrivateSentrySDKOnly.options.debug) {
NSData *data = [NSJSONSerialization dataWithJSONObject:contexts options:0 error:nil];
NSString *debugContext = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"Contexts: %@", debugContext);
}
}];

resolve(contexts);
}

Expand Down
29 changes: 28 additions & 1 deletion src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,35 @@ export type NativeReleaseResponse = {
version: string;
};

/**
* This type describes serialized scope from sentry-cocoa. (This is not used for Android)
* https://github.com/getsentry/sentry-cocoa/blob/master/Sources/Sentry/SentryScope.m
*/
export type NativeDeviceContextsResponse = {
[key: string]: Record<string, unknown>;
[key: string]: unknown;
tags?: Record<string, string>;
extra?: Record<string, unknown>;
context?: Record<string, Record<string, unknown>>;
user?: {
userId?: string;
email?: string;
username?: string;
ipAddress?: string;
segment?: string;
data?: Record<string, unknown>;
};
dist?: string;
environment?: string;
fingerprint?: string[];
level?: string;
breadcrumbs?: {
level?: string;
timestamp?: string;
category?: string;
type?: string;
message?: string;
data?: Record<string, unknown>;
}[];
};

export type NativeScreenshot = {
Expand Down
42 changes: 42 additions & 0 deletions src/js/breadcrumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Breadcrumb, SeverityLevel } from '@sentry/types';
import { severityLevelFromString } from '@sentry/utils';

export const DEFAULT_BREADCRUMB_LEVEL: SeverityLevel = 'error';

type BreadcrumbCandidate = {
[K in keyof Partial<Breadcrumb>]: unknown;
}

/**
* Convert plain object to a valid Breadcrumb
*/
export function breadcrumbFromObject(candidate: BreadcrumbCandidate): Breadcrumb {
const breadcrumb: Breadcrumb = {};

if (typeof candidate.type === 'string') {
breadcrumb.type = candidate.type;
}
if (typeof candidate.level === 'string') {
breadcrumb.level = severityLevelFromString(candidate.level);
}
if (typeof candidate.event_id === 'string') {
breadcrumb.event_id = candidate.event_id;
}
if (typeof candidate.category === 'string') {
breadcrumb.category = candidate.category;
}
if (typeof candidate.message === 'string') {
breadcrumb.message = candidate.message;
}
if (typeof candidate.data === 'object' && candidate.data !== null) {
breadcrumb.data = candidate.data;
}
if (typeof candidate.timestamp === 'string') {
const timestamp = Date.parse(candidate.timestamp)
if (!isNaN(timestamp)) {
breadcrumb.timestamp = timestamp;
}
}

return breadcrumb;
}
73 changes: 60 additions & 13 deletions src/js/integrations/devicecontext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
import { Contexts, Event, Integration } from '@sentry/types';
import { logger } from '@sentry/utils';
/* eslint-disable complexity */
import { Event, EventProcessor, Hub, Integration } from '@sentry/types';
import { logger, severityLevelFromString } from '@sentry/utils';

import { breadcrumbFromObject } from '../breadcrumb';
import { NativeDeviceContextsResponse } from '../NativeRNSentry';
import { NATIVE } from '../wrapper';

/** Load device context from native. */
Expand All @@ -19,26 +21,71 @@ export class DeviceContext implements Integration {
/**
* @inheritDoc
*/
public setupOnce(): void {
public setupOnce(
addGlobalEventProcessor: (callback: EventProcessor) => void,
getCurrentHub: () => Hub,
): void {
addGlobalEventProcessor(async (event: Event) => {
const self = getCurrentHub().getIntegration(DeviceContext);
if (!self) {
return event;
}

let native: NativeDeviceContextsResponse | null = null;
try {
const contexts = await NATIVE.fetchNativeDeviceContexts();
native = await NATIVE.fetchNativeDeviceContexts();
} catch (e) {
logger.log(`Failed to get device context from native: ${e}`);
}

if (!native) {
return event;
}

const context = contexts['context'] as Contexts ?? {};
const user = contexts['user'] ?? {};
const nativeUser = native.user;
if (!event.user && nativeUser) {
event.user = nativeUser;
}

event.contexts = { ...context, ...event.contexts };
const nativeContext = native.context;
if (nativeContext) {
event.contexts = { ...nativeContext, ...event.contexts };
}

if (!event.user) {
event.user = { ...user };
}
} catch (e) {
logger.log(`Failed to get device context from native: ${e}`);
const nativeTags = native.tags;
if (nativeTags) {
event.tags = { ...nativeTags, ...event.tags };
}

const nativeExtra = native.extra;
if (nativeExtra) {
event.extra = { ...nativeExtra, ...event.extra };
}

const nativeFingerprint = native.fingerprint;
if (nativeFingerprint) {
event.fingerprint = (event.fingerprint ?? []).concat(
nativeFingerprint.filter((item) => (event.fingerprint ?? []).indexOf(item) < 0),
)
}

const nativeLevel = typeof native['level'] === 'string'
? severityLevelFromString(native['level'])
: undefined;
if (!event.level && nativeLevel) {
event.level = nativeLevel;
}

const nativeEnvironment = native['environment'];
if (!event.environment && nativeEnvironment) {
event.environment = nativeEnvironment;
}

const nativeBreadcrumbs = Array.isArray(native['breadcrumbs'])
? native['breadcrumbs'].map(breadcrumbFromObject)
: undefined;
if (nativeBreadcrumbs) {
event.breadcrumbs = nativeBreadcrumbs;
}

return event;
Expand Down
9 changes: 7 additions & 2 deletions src/js/scope.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Scope } from '@sentry/core';
import { Attachment, Breadcrumb, User } from '@sentry/types';

import { DEFAULT_BREADCRUMB_LEVEL } from './breadcrumb';
import { NATIVE } from './wrapper';

/**
Expand Down Expand Up @@ -58,8 +59,12 @@ export class ReactNativeScope extends Scope {
* @inheritDoc
*/
public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this {
NATIVE.addBreadcrumb(breadcrumb);
return super.addBreadcrumb(breadcrumb, maxBreadcrumbs);
const mergedBreadcrumb = {
...breadcrumb,
level: breadcrumb.level || DEFAULT_BREADCRUMB_LEVEL,
};
NATIVE.addBreadcrumb(mergedBreadcrumb);
return super.addBreadcrumb(mergedBreadcrumb, maxBreadcrumbs);
}

/**
Expand Down
5 changes: 0 additions & 5 deletions src/js/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,11 +566,6 @@ export const NATIVE: SentryNativeWrapper = {
if (level == 'log' as SeverityLevel) {
return 'debug' as SeverityLevel;
}
else if (level == 'critical' as SeverityLevel) {
return 'fatal' as SeverityLevel;
}
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved


return level;
},

Expand Down
52 changes: 52 additions & 0 deletions test/breadcrumb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Breadcrumb } from '@sentry/types';

import { breadcrumbFromObject } from '../src/js/breadcrumb';

describe('Breadcrumb', () => {
describe('breadcrumbFromObject', () => {
it('convert a plain object to a valid Breadcrumb', () => {
const candidate = {
type: 'test',
level: 'info',
event_id: '1234',
category: 'test',
message: 'test',
data: {
test: 'test',
},
timestamp: '2020-01-01T00:00:00.000Z',
};
const breadcrumb = breadcrumbFromObject(candidate);
expect(breadcrumb).toEqual(<Breadcrumb>{
type: 'test',
level: 'info',
event_id: '1234',
category: 'test',
message: 'test',
data: {
test: 'test',
},
timestamp: 1577836800000,
});
});

it('convert plain object with invalid timestamp to a valid Breadcrumb', () => {
const candidate = {
type: 'test',
level: 'info',
timestamp: 'invalid',
};
const breadcrumb = breadcrumbFromObject(candidate);
expect(breadcrumb).toEqual(<Breadcrumb>{
type: 'test',
level: 'info',
});
});

it('convert empty object to a valid Breadcrumb', () => {
const candidate = {};
const breadcrumb = breadcrumbFromObject(candidate);
expect(breadcrumb).toEqual(<Breadcrumb>{});
});
});
});
Loading