diff --git a/README.md b/README.md index 73e49b14ff..597c89dddd 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,23 @@ With this SDK, Sentry is now able to provide mixed stacktraces. This means that if a JavaScript call causes a crash in native code, you will see the last call from JavaScript before the crash. This also means that with the new SDK, native crashes are properly handled on iOS. Full Android support coming soon but it will gracefully downgrade to use [raven-js](https://github.com/getsentry/raven-js). +## Additional device information + +When using this library you will get alot more information about the device surrounding your crashes. + +**Without native integration** +![Raven js only](https://github.com/getsentry/react-native-sentry/raw/master/assets/raven.png) + +**With native integration** +![Enriched](https://github.com/getsentry/react-native-sentry/raw/master/assets/enriched.png) +![Additional](https://github.com/getsentry/react-native-sentry/raw/master/assets/additional-device.png) + + +**Mixed Stacktraces**(1) ![Mixed Stacktrace](https://github.com/getsentry/react-native-sentry/raw/master/assets/mixed-stacktrace.png) ## Documentation https://docs.sentry.io/clients/react-native/ + +(1)only suppored on iOS diff --git a/android/build.gradle b/android/build.gradle index a07b6bd43e..fbb28d8297 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,5 +21,5 @@ android { dependencies { compile 'com.facebook.react:react-native:+' - compile 'io.sentry:sentry-android:1.0.0' + compile 'io.sentry:sentry-android:1.2.1' } diff --git a/android/src/main/java/io/sentry/ExceptionsManagerModuleInterface.java b/android/src/main/java/io/sentry/ExceptionsManagerModuleInterface.java deleted file mode 100644 index 799fcdab05..0000000000 --- a/android/src/main/java/io/sentry/ExceptionsManagerModuleInterface.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.sentry; - -import com.facebook.react.bridge.ReadableArray; - -public interface ExceptionsManagerModuleInterface { - - String MODULE_NAME = "RKExceptionsManager"; - - void reportFatalException(String title, ReadableArray details, int exceptionId); - - void reportSoftException(String title, ReadableArray details, int exceptionId); - - void updateExceptionMessage(String title, ReadableArray details, int exceptionId); - - void dismissRedbox(); - -} \ No newline at end of file diff --git a/android/src/main/java/io/sentry/RNSentryExceptionsManagerModule.java b/android/src/main/java/io/sentry/RNSentryExceptionsManagerModule.java deleted file mode 100644 index b3f05774ad..0000000000 --- a/android/src/main/java/io/sentry/RNSentryExceptionsManagerModule.java +++ /dev/null @@ -1,185 +0,0 @@ -package io.sentry; - -import com.facebook.common.logging.FLog; -import com.facebook.react.bridge.BaseJavaModule; -import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableNativeArray; -import com.facebook.react.bridge.ReadableNativeMap; -import com.facebook.react.bridge.ReadableType; -import com.facebook.react.common.ReactConstants; -import io.sentry.Sentry; -import io.sentry.event.Event; -import io.sentry.event.EventBuilder; -import io.sentry.event.interfaces.ExceptionInterface; -import io.sentry.event.interfaces.SentryException; -import io.sentry.event.interfaces.SentryStackTraceElement; -import io.sentry.event.interfaces.StackTraceInterface; - -import java.io.File; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - - -public class RNSentryExceptionsManagerModule extends BaseJavaModule implements ExceptionsManagerModuleInterface { - - /** - * @see com.facebook.react.modules.core.ExceptionsManagerModule#mJsModuleIdPattern - */ - private static final Pattern mJsModuleIdPattern = Pattern.compile("(?:^|[/\\\\])(\\d+\\.js)$"); - - static public ReadableMap lastReceivedException = null; - - @Override - public String getName() { - return MODULE_NAME; - } - - /** - * @see com.facebook.react.modules.core.ExceptionsManagerModule#stackFrameToModuleId(ReadableMap) - */ - private static String stackFrameToModuleId(ReadableMap frame) { - if (frame.hasKey("file") && - !frame.isNull("file") && - frame.getType("file") == ReadableType.String) { - final Matcher matcher = mJsModuleIdPattern.matcher(frame.getString("file")); - if (matcher.find()) { - return matcher.group(1) + ":"; - } - } - return ""; - } - - /** - * @see com.facebook.react.modules.core.ExceptionsManagerModule#stackTraceToString(String, ReadableArray) - */ - private static String stackTraceToString(String message, ReadableArray stack) { - StringBuilder stringBuilder = new StringBuilder(message).append(", stack:\n"); - for (int i = 0; i < stack.size(); i++) { - ReadableMap frame = stack.getMap(i); - stringBuilder - .append(frame.getString("methodName")) - .append("@") - .append(stackFrameToModuleId(frame)) - .append(frame.getInt("lineNumber")); - if (frame.hasKey("column") && - !frame.isNull("column") && - frame.getType("column") == ReadableType.Number) { - stringBuilder - .append(":") - .append(frame.getInt("column")); - } - stringBuilder.append("\n"); - } - return stringBuilder.toString(); - } - - @Override - public boolean canOverrideExistingModule() { - return true; - } - - @ReactMethod - @Override - public void reportFatalException(String title, ReadableArray details, int exceptionId) { - convertAndCaptureReactNativeException(title, (ReadableNativeArray)details); - System.exit(0); - } - - @ReactMethod - @Override - public void reportSoftException(String title, ReadableArray details, int exceptionId) { - convertAndCaptureReactNativeException(title, (ReadableNativeArray)details); - } - - @ReactMethod - @Override - public void updateExceptionMessage(String title, ReadableArray details, int exceptionId) { - // Do nothing - } - - @ReactMethod - @Override - public void dismissRedbox() { - // Do nothing - } - - public static void convertAndCaptureReactNativeException(String title, ReadableNativeArray stack) { - StackTraceInterface stackTraceInterface = new StackTraceInterface(convertToNativeStacktrace(stack)); - Deque exceptions = new ArrayDeque<>(); - - String type = title; - String value = title; - if (null != RNSentryExceptionsManagerModule.lastReceivedException) { - ReadableNativeArray exceptionValues = ((ReadableNativeArray)RNSentryExceptionsManagerModule.lastReceivedException.getMap("exception").getArray("values")); - ReadableNativeMap exception = exceptionValues.getMap(0); - type = exception.getString("type"); - value = exception.getString("value"); - } - - exceptions.push(new SentryException(value, type, "", stackTraceInterface)); - EventBuilder eventBuilder = new EventBuilder() - .withLevel(Event.Level.FATAL) - .withSentryInterface(new ExceptionInterface(exceptions)); - Sentry.capture(RNSentryModule.buildEvent(eventBuilder)); - } - - private static SentryStackTraceElement[] convertToNativeStacktrace(ReadableNativeArray stack) { - final int stackFrameSize = stack.size(); - SentryStackTraceElement[] synthStackTrace = new SentryStackTraceElement[stackFrameSize]; - for (int i = 0; i < stackFrameSize; i++) { - ReadableNativeMap frame = stack.getMap(i); - - String fileName = ""; - if (frame.hasKey("file")) { - fileName = frame.getString("file"); - } else if (frame.hasKey("filename")) { - fileName = frame.getString("filename"); - } - - String methodName = ""; - if (frame.hasKey("methodName")) { - methodName = frame.getString("methodName"); - } else if (frame.hasKey("function")) { - methodName = frame.getString("function"); - } - - int lineNumber = 0; - if (frame.hasKey("lineNumber") && - !frame.isNull("lineNumber") && - frame.getType("lineNumber") == ReadableType.Number) { - lineNumber = frame.getInt("lineNumber"); - } else if (frame.hasKey("lineno") && - !frame.isNull("lineno") && - frame.getType("lineno") == ReadableType.Number) { - lineNumber = frame.getInt("lineno"); - } - - int column = 0; - if (frame.hasKey("column") && - !frame.isNull("column") && - frame.getType("column") == ReadableType.Number) { - column = frame.getInt("column"); - } else if (frame.hasKey("colno") && - !frame.isNull("colno") && - frame.getType("colno") == ReadableType.Number) { - column = frame.getInt("colno"); - } - - String[] lastFileNameSegments = fileName.split("\\?"); - String lastPathComponent = lastFileNameSegments[0]; - String[] fileNameSegments = lastPathComponent.split("/"); - StringBuilder finalFileName = new StringBuilder("app:///").append(fileNameSegments[fileNameSegments.length-1]); - - SentryStackTraceElement stackFrame = new SentryStackTraceElement("", methodName, stackFrameToModuleId(frame), lineNumber, column, finalFileName.toString(), "javascript"); - synthStackTrace[i] = stackFrame; - } - return synthStackTrace; - } - -} diff --git a/android/src/main/java/io/sentry/RNSentryModule.java b/android/src/main/java/io/sentry/RNSentryModule.java index 834ec46704..abea5efe64 100644 --- a/android/src/main/java/io/sentry/RNSentryModule.java +++ b/android/src/main/java/io/sentry/RNSentryModule.java @@ -14,13 +14,18 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeArray; import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import io.sentry.android.AndroidSentryClientFactory; import io.sentry.android.event.helper.AndroidEventBuilderHelper; @@ -31,10 +36,17 @@ import io.sentry.event.EventBuilder; import io.sentry.event.User; import io.sentry.event.UserBuilder; +import io.sentry.event.helper.ShouldSendEventCallback; +import io.sentry.event.interfaces.ExceptionInterface; +import io.sentry.event.interfaces.SentryException; +import io.sentry.event.interfaces.SentryStackTraceElement; +import io.sentry.event.interfaces.StackTraceInterface; import io.sentry.event.interfaces.UserInterface; public class RNSentryModule extends ReactContextBaseJavaModule { + private static final Pattern mJsModuleIdPattern = Pattern.compile("(?:^|[/\\\\])(\\d+\\.js)$"); + private final ReactApplicationContext reactContext; private final ReactApplication reactApplication; @@ -86,6 +98,20 @@ public void onSuccess(Event event) { RNSentryEventEmitter.sendEvent(reactContext, RNSentryEventEmitter.SENTRY_EVENT_SENT_SUCCESSFULLY, params); } }); + sentryClient.addShouldSendEventCallback(new ShouldSendEventCallback() { + @Override + public boolean shouldSend(Event event) { + // We don't want to send events that are from ExceptionsManagerModule. + // Because we sent it already from raven. + if (event.getSentryInterfaces().containsKey(ExceptionInterface.EXCEPTION_INTERFACE)) { + ExceptionInterface exceptionInterface = ((ExceptionInterface)event.getSentryInterfaces().get(ExceptionInterface.EXCEPTION_INTERFACE)); + if (exceptionInterface.getExceptions().getFirst().getExceptionClassName().contains("JavascriptException")) { + return false; + } + } + return true; + } + }); logger.info(String.format("startWithDsnString '%s'", dsnString)); } @@ -141,51 +167,52 @@ public void captureBreadcrumb(ReadableMap breadcrumb) { @ReactMethod public void captureEvent(ReadableMap event) { ReadableNativeMap castEvent = (ReadableNativeMap)event; + + EventBuilder eventBuilder = new EventBuilder() + .withLevel(eventLevel(castEvent)); + if (event.hasKey("message")) { - EventBuilder eventBuilder = new EventBuilder() - .withMessage(event.getString("message")) - .withLevel(eventLevel(castEvent)); + eventBuilder.withMessage(event.getString("message")); + } - if (event.hasKey("logger")) { - eventBuilder.withLogger(event.getString("logger")); - } + if (event.hasKey("logger")) { + eventBuilder.withLogger(event.getString("logger")); + } - if (event.hasKey("user")) { - UserBuilder userBuilder = getUserBuilder(event.getMap("user")); - User builtUser = userBuilder.build(); - if (builtUser.getId() != null) { - UserInterface userInterface = new UserInterface( - builtUser.getId(), - builtUser.getUsername(), - null, - builtUser.getEmail() - ); - eventBuilder.withSentryInterface(userInterface); - } + if (event.hasKey("user")) { + UserBuilder userBuilder = getUserBuilder(event.getMap("user")); + User builtUser = userBuilder.build(); + if (builtUser.getId() != null) { + UserInterface userInterface = new UserInterface( + builtUser.getId(), + builtUser.getUsername(), + null, + builtUser.getEmail() + ); + eventBuilder.withSentryInterface(userInterface); } + } - if (castEvent.hasKey("extra")) { - for (Map.Entry entry : castEvent.getMap("extra").toHashMap().entrySet()) { - eventBuilder.withExtra(entry.getKey(), entry.getValue()); - } + if (castEvent.hasKey("extra")) { + for (Map.Entry entry : castEvent.getMap("extra").toHashMap().entrySet()) { + eventBuilder.withExtra(entry.getKey(), entry.getValue()); } + } - if (castEvent.hasKey("tags")) { - for (Map.Entry entry : castEvent.getMap("tags").toHashMap().entrySet()) { - eventBuilder.withTag(entry.getKey(), entry.getValue().toString()); - } + if (castEvent.hasKey("tags")) { + for (Map.Entry entry : castEvent.getMap("tags").toHashMap().entrySet()) { + eventBuilder.withTag(entry.getKey(), entry.getValue().toString()); } + } - Sentry.capture(buildEvent(eventBuilder)); - } else { - RNSentryExceptionsManagerModule.lastReceivedException = event; - if (this.getReactApplication().getReactNativeHost().getUseDeveloperSupport() == true) { - ReadableNativeArray exceptionValues = ((ReadableNativeArray)RNSentryExceptionsManagerModule.lastReceivedException.getMap("exception").getArray("values")); - ReadableNativeMap exception = exceptionValues.getMap(0); - ReadableNativeMap stacktrace = exception.getMap("stacktrace"); - RNSentryExceptionsManagerModule.convertAndCaptureReactNativeException("", stacktrace.getArray("frames")); - } + if (event.hasKey("exception")) { + ReadableNativeArray exceptionValues = (ReadableNativeArray)event.getMap("exception").getArray("values"); + ReadableNativeMap exception = exceptionValues.getMap(0); + ReadableNativeMap stacktrace = exception.getMap("stacktrace"); + addExceptionInterface(eventBuilder, exception.getString("type"), exception.getString("value"), stacktrace.getArray("frames")); } + + Sentry.capture(buildEvent(eventBuilder)); } @ReactMethod @@ -242,6 +269,80 @@ public static Event buildEvent(EventBuilder eventBuilder) { return eventBuilder.build(); } + private static void addExceptionInterface(EventBuilder eventBuilder, String type, String value, ReadableNativeArray stack) { + StackTraceInterface stackTraceInterface = new StackTraceInterface(convertToNativeStacktrace(stack)); + Deque exceptions = new ArrayDeque<>(); + + exceptions.push(new SentryException(value, type, "", stackTraceInterface)); + + eventBuilder.withSentryInterface(new ExceptionInterface(exceptions)); + } + + private static SentryStackTraceElement[] convertToNativeStacktrace(ReadableNativeArray stack) { + final int stackFrameSize = stack.size(); + SentryStackTraceElement[] synthStackTrace = new SentryStackTraceElement[stackFrameSize]; + for (int i = 0; i < stackFrameSize; i++) { + ReadableNativeMap frame = stack.getMap(i); + + String fileName = ""; + if (frame.hasKey("file")) { + fileName = frame.getString("file"); + } else if (frame.hasKey("filename")) { + fileName = frame.getString("filename"); + } + + String methodName = ""; + if (frame.hasKey("methodName")) { + methodName = frame.getString("methodName"); + } else if (frame.hasKey("function")) { + methodName = frame.getString("function"); + } + + int lineNumber = 0; + if (frame.hasKey("lineNumber") && + !frame.isNull("lineNumber") && + frame.getType("lineNumber") == ReadableType.Number) { + lineNumber = frame.getInt("lineNumber"); + } else if (frame.hasKey("lineno") && + !frame.isNull("lineno") && + frame.getType("lineno") == ReadableType.Number) { + lineNumber = frame.getInt("lineno"); + } + + int column = 0; + if (frame.hasKey("column") && + !frame.isNull("column") && + frame.getType("column") == ReadableType.Number) { + column = frame.getInt("column"); + } else if (frame.hasKey("colno") && + !frame.isNull("colno") && + frame.getType("colno") == ReadableType.Number) { + column = frame.getInt("colno"); + } + + String[] lastFileNameSegments = fileName.split("\\?"); + String lastPathComponent = lastFileNameSegments[0]; + String[] fileNameSegments = lastPathComponent.split("/"); + StringBuilder finalFileName = new StringBuilder("app:///").append(fileNameSegments[fileNameSegments.length-1]); + + SentryStackTraceElement stackFrame = new SentryStackTraceElement("", methodName, stackFrameToModuleId(frame), lineNumber, column, finalFileName.toString(), "javascript"); + synthStackTrace[i] = stackFrame; + } + return synthStackTrace; + } + + private static String stackFrameToModuleId(ReadableMap frame) { + if (frame.hasKey("file") && + !frame.isNull("file") && + frame.getType("file") == ReadableType.String) { + final Matcher matcher = mJsModuleIdPattern.matcher(frame.getString("file")); + if (matcher.find()) { + return matcher.group(1) + ":"; + } + } + return ""; + } + private static void stripInternalSentry(EventBuilder eventBuilder) { if (extra != null) { for (Map.Entry entry : extra.toHashMap().entrySet()) { diff --git a/android/src/main/java/io/sentry/RNSentryPackage.java b/android/src/main/java/io/sentry/RNSentryPackage.java index c6e3328294..51df25e865 100644 --- a/android/src/main/java/io/sentry/RNSentryPackage.java +++ b/android/src/main/java/io/sentry/RNSentryPackage.java @@ -26,9 +26,6 @@ public ReactApplication getReactApplication() { @Override public List createNativeModules(ReactApplicationContext reactContext) { - if (!this.getReactApplication().getReactNativeHost().getUseDeveloperSupport()) { - return Arrays.asList(new RNSentryModule(reactContext, this.getReactApplication()), new RNSentryEventEmitter(reactContext), new RNSentryExceptionsManagerModule()); - } return Arrays.asList(new RNSentryModule(reactContext, this.getReactApplication()), new RNSentryEventEmitter(reactContext)); } diff --git a/android/src/main/java/io/sentry/ReactNativeException.java b/android/src/main/java/io/sentry/ReactNativeException.java deleted file mode 100644 index 6a4f2746b5..0000000000 --- a/android/src/main/java/io/sentry/ReactNativeException.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.sentry; - -public class ReactNativeException extends RuntimeException { - public ReactNativeException() { - super(); - } - - public ReactNativeException(String message) { - super(message); - } - - public ReactNativeException(String message, Throwable cause) { - super(message, cause); - } - - public ReactNativeException(Throwable cause) { - super(cause); - } -} diff --git a/assets/additional-device.png b/assets/additional-device.png new file mode 100644 index 0000000000..18374dc107 Binary files /dev/null and b/assets/additional-device.png differ diff --git a/assets/enriched.png b/assets/enriched.png new file mode 100644 index 0000000000..18374dc107 Binary files /dev/null and b/assets/enriched.png differ diff --git a/assets/raven.png b/assets/raven.png new file mode 100644 index 0000000000..f3c193c2a1 Binary files /dev/null and b/assets/raven.png differ diff --git a/docs/config.rst b/docs/config.rst index d4bab2debc..bcda6e64f1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -1,5 +1,5 @@ Additional Configuration -======================== +------------------------ These are functions you can call in your javascript code: diff --git a/docs/index.rst b/docs/index.rst index f1fcf8b2a7..0d470067b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,6 +88,13 @@ Note: When you run ``react-native link`` we will automatically update your You can pass additional configuration options to the `config()` method if you want to do so. +Mixed Stacktraces +----------------- + +Currently we only support mixed stacktraces on iOS. By default this feature is +enabled. If you encounter performance issues we recommend try turning it +off ``deactivateStacktraceMerging: true`` see: :doc:`config`. + Deep Dive --------- diff --git a/ios/RNSentry.m b/ios/RNSentry.m index 777932b7e0..b02a0e87fc 100644 --- a/ios/RNSentry.m +++ b/ios/RNSentry.m @@ -23,12 +23,11 @@ - (dispatch_queue_t)methodQueue } + (void)installWithBridge:(RCTBridge *)bridge { - RNSentry *sentry = [bridge moduleForName:@"RNSentry"]; - [[bridge moduleForName:@"ExceptionsManager"] initWithDelegate:sentry]; + // For now we don't need this anymore } + (void)installWithRootView:(RCTRootView *)rootView { - [RNSentry installWithBridge: rootView.bridge]; + // For now we don't need this anymore } + (NSNumberFormatter *)numberFormatter { @@ -110,7 +109,7 @@ - (NSInteger)indexOfReactNativeCallFrame:(NSArray *)frames native return index; } -- (NSArray *)convertReactNativeStacktrace:(NSDictionary *)stacktrace { +- (NSArray *)convertReactNativeStacktrace:(NSArray *)stacktrace { NSMutableArray *frames = [NSMutableArray new]; for (NSDictionary *frame in stacktrace) { if (nil == frame[@"methodName"]) { @@ -146,7 +145,7 @@ - (void)injectReactNativeFrames:(SentryEvent *)event { NSMutableArray *finalFrames = [NSMutableArray new]; - NSArray *reactFrames = [self convertReactNativeStacktrace:event.extra[@"__sentry_stack"]]; + NSArray *reactFrames = [self convertReactNativeStacktrace:SentryParseJavaScriptStacktrace(event.extra[@"__sentry_stack"])]; for (NSInteger i = 0; i < frames.count; i++) { [finalFrames addObject:[frames objectAtIndex:i]]; if (i == indexOfReactFrames) { @@ -189,6 +188,16 @@ - (void)setReleaseVersionDist:(SentryEvent *)event { if (error) { [NSException raise:@"SentryReactNative" format:@"%@", error.localizedDescription]; } + SentryClient.sharedClient.shouldSendEvent = ^BOOL(SentryEvent * _Nonnull event) { + // We don't want to send an event after startup that came from a NSException of react native + // Because we sent it already before the app crashed. + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"RCTFatalException"].location != NSNotFound) { + NSLog(@"RCTFatalException"); + return NO; + } + return YES; + }; SentryClient.sharedClient.beforeSerializeEvent = ^(SentryEvent * _Nonnull event) { [self injectReactNativeFrames:event]; [self setReleaseVersionDist:event]; @@ -225,7 +234,7 @@ - (void)swizzleInvokeWithBridge:(Class)class { @synchronized (SentryClient.sharedClient) { NSMutableDictionary *prevExtra = SentryClient.sharedClient.extra.mutableCopy; [prevExtra setValue:[NSNumber numberWithUnsignedInteger:callNativeModuleAddress] forKey:@"__sentry_address"]; - [prevExtra setValue:SentryParseJavaScriptStacktrace([RCTConvert NSString:param[@"__sentry_stack"]]) forKey:@"__sentry_stack"]; + [prevExtra setValue:[RCTConvert NSString:param[@"__sentry_stack"]] forKey:@"__sentry_stack"]; SentryClient.sharedClient.extra = prevExtra; } } else { @@ -257,7 +266,7 @@ - (void)swizzleCallNativeModule:(Class)class { @synchronized (SentryClient.sharedClient) { NSMutableDictionary *prevExtra = SentryClient.sharedClient.extra.mutableCopy; [prevExtra setValue:[NSNumber numberWithUnsignedInteger:callNativeModuleAddress] forKey:@"__sentry_address"]; - [prevExtra setValue:SentryParseJavaScriptStacktrace([RCTConvert NSString:param[@"__sentry_stack"]]) forKey:@"__sentry_stack"]; + [prevExtra setValue:[RCTConvert NSString:param[@"__sentry_stack"]] forKey:@"__sentry_stack"]; SentryClient.sharedClient.extra = prevExtra; } } else { @@ -329,20 +338,33 @@ - (void)swizzleCallNativeModule:(Class)class { user.username = [NSString stringWithFormat:@"%@", event[@"user"][@"username"]]; user.extra = [RCTConvert NSDictionary:event[@"user"][@"extra"]]; } - - if (event[@"message"]) { - SentryEvent *sentryEvent = [[SentryEvent alloc] initWithLevel:level]; - sentryEvent.eventId = event[@"event_id"]; - sentryEvent.message = event[@"message"]; - sentryEvent.logger = event[@"logger"]; - sentryEvent.tags = [self sanitizeDictionary:event[@"tags"]]; - sentryEvent.extra = event[@"extra"]; - sentryEvent.user = user; - [SentryClient.sharedClient sendEvent:sentryEvent withCompletionHandler:NULL]; - } else if (event[@"exception"]) { - self.lastReceivedException = event; + + SentryEvent *sentryEvent = [[SentryEvent alloc] initWithLevel:level]; + sentryEvent.eventId = event[@"event_id"]; + sentryEvent.message = event[@"message"]; + sentryEvent.logger = event[@"logger"]; + sentryEvent.tags = [self sanitizeDictionary:event[@"tags"]]; + sentryEvent.extra = event[@"extra"]; + sentryEvent.user = user; + if (event[@"exception"]) { + NSDictionary *exception = event[@"exception"][@"values"][0]; + NSMutableArray *frames = [NSMutableArray array]; + NSArray *stacktrace = [self convertReactNativeStacktrace:SentryParseRavenFrames(exception[@"stacktrace"][@"frames"])]; + for (NSInteger i = (stacktrace.count-1); i > 0; i--) { + [frames addObject:[stacktrace objectAtIndex:i]]; + } + [self addExceptionToEvent:sentryEvent type:exception[@"type"] value:exception[@"value"] frames:frames]; } + [SentryClient.sharedClient sendEvent:sentryEvent withCompletionHandler:NULL]; +} +- (void)addExceptionToEvent:(SentryEvent *)event type:(NSString *)type value:(NSString *)value frames:(NSArray *)frames { + SentryException *sentryException = [[SentryException alloc] initWithValue:value type:type]; + SentryThread *thread = [[SentryThread alloc] initWithThreadId:@(99)]; + thread.crashed = @(YES); + thread.stacktrace = [[SentryStacktrace alloc] initWithFrames:frames registers:@{}]; + sentryException.thread = thread; + event.exceptions = @[sentryException]; } RCT_EXPORT_METHOD(crash) @@ -386,30 +408,4 @@ - (NSDictionary *)sanitizeDictionary:(NSDictionary *)dictionary { return [NSDictionary dictionaryWithDictionary:dict]; } -- (void)reportReactNativeCrashWithMessage:(NSString *)message stacktrace:(NSArray *)stack terminateProgram:(BOOL)terminateProgram { - NSString *newMessage = message; - if (nil != self.lastReceivedException) { - newMessage = [NSString stringWithFormat:@"%@:%@", self.lastReceivedException[@"exception"][@"values"][0][@"type"], self.lastReceivedException[@"exception"][@"values"][0][@"value"]]; - } - [SentryClient.sharedClient reportUserException:@"ReactNativeException" reason:newMessage language:@"cocoa" lineOfCode:@"" stackTrace:stack logAllThreads:YES terminateProgram:terminateProgram]; -} - -#pragma mark RCTExceptionsManagerDelegate - -- (void)handleSoftJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId { - [self reportReactNativeCrashWithMessage:message stacktrace:stack terminateProgram:NO]; -} - -- (void)handleFatalJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack exceptionId:(NSNumber *)exceptionId { -#ifndef DEBUG - RCTSetFatalHandler(^(NSError *error) { - [self reportReactNativeCrashWithMessage:message stacktrace:stack terminateProgram:YES]; - }); -#else - RCTSetFatalHandler(^(NSError *error) { - [self reportReactNativeCrashWithMessage:message stacktrace:stack terminateProgram:NO]; - }); -#endif -} - @end diff --git a/ios/Sentry b/ios/Sentry index ff7c590bfd..2ffded7415 160000 --- a/ios/Sentry +++ b/ios/Sentry @@ -1 +1 @@ -Subproject commit ff7c590bfd7f1be6bfd94e2800bd8d0a73e0dac1 +Subproject commit 2ffded741538533ccb71d51cac1311ea65f1e6d9 diff --git a/lib/raven-plugin.js b/lib/raven-plugin.js index eb9adffad6..66b81c85a5 100644 --- a/lib/raven-plugin.js +++ b/lib/raven-plugin.js @@ -89,16 +89,6 @@ function reactNativePlugin(Raven, options, internalDataCallback) { }); })['catch'](function() {}); - // Make sure that if multiple fatals occur, we only persist the first one. - // - // The first error is probably the most important/interesting error, and we - // want to crash ASAP, rather than potentially queueing up multiple errors. - var handlingFatal = false; - - var defaultHandler = - (ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler()) || - ErrorUtils._globalHandler; - Raven.setShouldSendCallback(function(data, originalCallback) { if (!(FATAL_ERROR_KEY in data)) { // not a fatal (will not crash runtime), continue as planned @@ -117,6 +107,16 @@ function reactNativePlugin(Raven, options, internalDataCallback) { return false; // Do not continue. }); + // Make sure that if multiple fatals occur, we only persist the first one. + // + // The first error is probably the most important/interesting error, and we + // want to crash ASAP, rather than potentially queueing up multiple errors. + var handlingFatal = false; + + var defaultHandler = + (ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler()) || + ErrorUtils._globalHandler; + ErrorUtils.setGlobalHandler(function(error, isFatal) { var captureOptions = { timestamp: new Date() / 1000 @@ -135,10 +135,8 @@ function reactNativePlugin(Raven, options, internalDataCallback) { captureOptions[FATAL_ERROR_KEY] = error; } Raven.captureException(error, captureOptions); - // Handle non-fatals regularly. - if (!shouldHandleFatal) { - defaultHandler(error); - } + // We always want to tunnel errors to the default handler + defaultHandler(error, isFatal); }); } diff --git a/package.json b/package.json index 5faa6ddf00..67e9e97e83 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "glob": "7.1.1", "inquirer": "3.0.6", "raven-js": "^3.16.0", - "sentry-cli-binary": "^1.13.3", + "sentry-cli-binary": "^1.16.0", "xcode": "0.9.3" }, "rnpm": {