Skip to content

Commit 629708b

Browse files
pvinisfacebook-github-bot
authored andcommitted
Crash reporting heaven (#23691)
Summary: <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? --> I have used RN for a long time, and for all this time, crash reporting has been less great than native development crash reporting. At some point, companies like sentry, bugsnag and a bunch of others started supporting sourcemaps for js crashes in RN, which helped a lot. But native crashes were (and still are) much harder to diagnose. ..Until now :D I have make a repo of a sample RN app, included this PR in it, and some code and screenshots to help. The repo is [here](https://github.com/pvinis/react-native-project-with-crash-heaven-pr). I was trying to get good crash reports from native crashes in iOS for a looong time. I spoke with people in sentry, in bugsnag and more, and I could not get this solved. There was no clear way to get the **native** crashed to display correctly. I made two repos here, one for [sentry](https://github.com/pvinis/SentryBadStack) and one for [bugsnag](https://github.com/pvinis/BugsnagBadStack), demonstrating the correct js handling and the bad native handling. After all this, and talks with their support, twitter etc, I investigated further, on **why** this was happening. I thought there must be some reason that native crashes look bad in all the tools, and in the same way. Maybe it's not their fault, or up to them to fix it, or maybe they didn't have the experience to fix it. In a test project I created, I checked what's up with the `RCTFatalException`, and I found out that the React Native code is catching the `NSException`s that come from any native modules of a RN app and converting it into an string and sending it to `RCTFatal` that created an `NSError` out of that string. Then it checks if the app has set a fatal error handler and if not, goes ahead and throws that `NSError`. The problem here is that `NSException` has a bunch more info that the resulting `NSError` is missing or is altering. Turning the callstack into a string renders crash reporting tools useless as they are missing the original place the exception was thrown, symbols, return addresses etc. In both repos above it can be seen that both tools were thinking that the error happened somewhere in the `RCTFatal` function, and it did, since we create it there, losing all the previous useful info of the original exception. That leaves us with just a very long name including a callstack, but very hard to actually map this to the code and dsym. I added a fatal exception handler, that mirrors the fatal error handler, as the error handler is used around React Native internal code. Then I stopped making a string out of the original `NSException` and calling `RCTFatal`, and I simply throw the exception. This way no info is lost! Finally, I added some code examples of native and js crashes and added a part in the `RNTester` app, so people can see how a js and a native error look like while debugging, as well as try to compile the app in release mode and see how the crash report would look like if they connect it to bugsnag or sentry or their tool of choice. I have attached some images at the bottom of this PR, and you can find some in the 3 repos I linked above. [iOS] [Fixed] - Changed the way iOS native module exceptions get handled. Instead of making them into an `NSError` and lose the context and callstack, we keep them as `NSException`s and propagate them. [General] [Added] - Example code for native crashes in iOS and Android, with buttons on RNTester, so developers can see how these look when debugging, as well as the crash reports in release mode. Pull Request resolved: #23691 Reviewed By: fkgozali Differential Revision: D14276366 Pulled By: cpojer fbshipit-source-id: b308d5608e1432d7676447347ae77c0721094e62
1 parent c991e1c commit 629708b

File tree

9 files changed

+161
-4
lines changed

9 files changed

+161
-4
lines changed

RNTester/RNTester.xcodeproj/project.pbxproj

+15
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; };
127127
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; };
128128
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */; };
129+
AFEACA842223EB05004E5198 /* CrashyCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = AFEACA832223EB05004E5198 /* CrashyCrash.m */; };
129130
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */; };
130131
C60A228221C9726800B820FE /* RCTFormatErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C60A228121C9726800B820FE /* RCTFormatErrorTests.m */; };
131132
C60A228321C9726800B820FE /* RCTFormatErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C60A228121C9726800B820FE /* RCTFormatErrorTests.m */; };
@@ -563,6 +564,8 @@
563564
8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = "<group>"; };
564565
8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = "<group>"; };
565566
8385CF051B8747A000C6273E /* RCTImageLoaderHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageLoaderHelpers.h; sourceTree = "<group>"; };
567+
AFEACA822223EB05004E5198 /* CrashyCrash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CrashyCrash.h; sourceTree = "<group>"; };
568+
AFEACA832223EB05004E5198 /* CrashyCrash.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashyCrash.m; sourceTree = "<group>"; };
566569
BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenuTests.m; sourceTree = "<group>"; };
567570
C60A228121C9726800B820FE /* RCTFormatErrorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTFormatErrorTests.m; sourceTree = "<group>"; };
568571
C654F0B21EB34A73000B7A9A /* RNTesterTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterTestModule.m; sourceTree = "<group>"; };
@@ -774,6 +777,7 @@
774777
272E6B3A1BEA846C001FCF37 /* NativeExampleViews */,
775778
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
776779
13B07FB01A68108700A75B9A /* AppDelegate.m */,
780+
AFEACAB12223EB2C004E5198 /* NativeExampleModules */,
777781
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
778782
13B07FB71A68108700A75B9A /* main.m */,
779783
1323F18D1C04ABAC0091BED0 /* Supporting Files */,
@@ -1021,6 +1025,16 @@
10211025
name = Products;
10221026
sourceTree = "<group>";
10231027
};
1028+
AFEACAB12223EB2C004E5198 /* NativeExampleModules */ = {
1029+
isa = PBXGroup;
1030+
children = (
1031+
AFEACA822223EB05004E5198 /* CrashyCrash.h */,
1032+
AFEACA832223EB05004E5198 /* CrashyCrash.m */,
1033+
);
1034+
name = NativeExampleModules;
1035+
path = RNTester/NativeExampleModules;
1036+
sourceTree = "<group>";
1037+
};
10241038
D85B82921AB6D5CE003F4FE2 /* Products */ = {
10251039
isa = PBXGroup;
10261040
children = (
@@ -1724,6 +1738,7 @@
17241738
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
17251739
27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */,
17261740
13B07FC11A68108700A75B9A /* main.m in Sources */,
1741+
AFEACA842223EB05004E5198 /* CrashyCrash.m in Sources */,
17271742
);
17281743
runOnlyForDeploymentPostprocessing = 0;
17291744
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
#import <Foundation/Foundation.h>
10+
#import <React/RCTBridgeModule.h>
11+
12+
13+
NS_ASSUME_NONNULL_BEGIN
14+
15+
@interface CrashyCrash : NSObject <RCTBridgeModule>
16+
@end
17+
18+
NS_ASSUME_NONNULL_END
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
#import "CrashyCrash.h"
10+
11+
12+
@implementation CrashyCrash
13+
14+
RCT_EXPORT_MODULE();
15+
16+
RCT_EXPORT_METHOD(letsCrash)
17+
{
18+
NSArray *a = @[@"wow"];
19+
NSString *s = [a objectAtIndex:42]; // native crash here
20+
NSLog(@"%@", s);
21+
}
22+
23+
@end

RNTester/js/CrashExample.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict-local
9+
*/
10+
11+
'use strict';
12+
13+
import React from 'react';
14+
import {NativeModules, Button} from 'react-native';
15+
16+
const {CrashyCrash} = NativeModules;
17+
18+
exports.displayName = (undefined: ?string);
19+
exports.framework = 'React';
20+
exports.title = 'Crash';
21+
exports.description = 'Crash examples.';
22+
23+
exports.examples = [
24+
{
25+
title: 'JS crash',
26+
render() {
27+
return (
28+
<Button
29+
title="JS crash"
30+
onPress={() => {
31+
const a = {};
32+
const b = a.w.q; // js crash here
33+
console.log(b);
34+
}}
35+
/>
36+
);
37+
},
38+
},
39+
{
40+
title: 'Native crash',
41+
render() {
42+
return (
43+
<Button
44+
title="Native crash"
45+
onPress={() => {
46+
CrashyCrash.letsCrash();
47+
}}
48+
/>
49+
);
50+
},
51+
},
52+
];

RNTester/js/RNTesterList.android.js

+4
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ const APIExamples: Array<RNTesterExample> = [
152152
key: 'ClipboardExample',
153153
module: require('./ClipboardExample'),
154154
},
155+
{
156+
key: 'CrashExample',
157+
module: require('./CrashExample'),
158+
},
155159
{
156160
key: 'DatePickerAndroidExample',
157161
module: require('./DatePickerAndroidExample'),

RNTester/js/RNTesterList.ios.js

+5
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ const APIExamples: Array<RNTesterExample> = [
231231
module: require('./ClipboardExample'),
232232
supportsTVOS: false,
233233
},
234+
{
235+
key: 'CrashExample',
236+
module: require('./CrashExample'),
237+
supportsTVOS: false,
238+
},
234239
{
235240
key: 'Dimensions',
236241
module: require('./DimensionsExample'),

React/Base/RCTAssert.h

+8-2
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ RCT_EXTERN void _RCTAssertFormat(
3939
/**
4040
* Report a fatal condition when executing. These calls will _NOT_ be compiled out
4141
* in production, and crash the app by default. You can customize the fatal behaviour
42-
* by setting a custom fatal handler through `RCTSetFatalHandler`.
42+
* by setting a custom fatal handler through `RCTSetFatalHandler` and
43+
* `RCTSetFatalExceptionHandler`.
4344
*/
4445
RCT_EXTERN void RCTFatal(NSError *error);
46+
RCT_EXTERN void RCTFatalException(NSException *exception);
4547

4648
/**
4749
* The default error domain to be used for React errors.
@@ -73,6 +75,7 @@ typedef void (^RCTAssertFunction)(NSString *condition,
7375
NSString *message);
7476

7577
typedef void (^RCTFatalHandler)(NSError *error);
78+
typedef void (^RCTFatalExceptionHandler)(NSException *exception);
7679

7780
/**
7881
* Convenience macro for asserting that a parameter is non-nil/non-zero.
@@ -114,10 +117,13 @@ RCT_EXTERN void RCTAddAssertFunction(RCTAssertFunction assertFunction);
114117
RCT_EXTERN void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction);
115118

116119
/**
117-
These methods get and set the current fatal handler called by the RCTFatal method.
120+
* These methods get and set the current fatal handler called by the `RCTFatal`
121+
* and `RCTFatalException` methods.
118122
*/
119123
RCT_EXTERN void RCTSetFatalHandler(RCTFatalHandler fatalHandler);
120124
RCT_EXTERN RCTFatalHandler RCTGetFatalHandler(void);
125+
RCT_EXTERN void RCTSetFatalExceptionHandler(RCTFatalExceptionHandler fatalExceptionHandler);
126+
RCT_EXTERN RCTFatalExceptionHandler RCTGetFatalExceptionHandler(void);
121127

122128
/**
123129
* Get the current thread's name (or the current queue, if in debug mode)

React/Base/RCTAssert.m

+32-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
RCTAssertFunction RCTCurrentAssertFunction = nil;
2020
RCTFatalHandler RCTCurrentFatalHandler = nil;
21+
RCTFatalExceptionHandler RCTCurrentFatalExceptionHandler = nil;
2122

2223
NSException *_RCTNotImplementedException(SEL, Class);
2324
NSException *_RCTNotImplementedException(SEL cmd, Class cls)
@@ -149,9 +150,9 @@ void RCTFatal(NSError *error)
149150
}
150151
}
151152

152-
void RCTSetFatalHandler(RCTFatalHandler fatalhandler)
153+
void RCTSetFatalHandler(RCTFatalHandler fatalHandler)
153154
{
154-
RCTCurrentFatalHandler = fatalhandler;
155+
RCTCurrentFatalHandler = fatalHandler;
155156
}
156157

157158
RCTFatalHandler RCTGetFatalHandler(void)
@@ -187,3 +188,32 @@ RCTFatalHandler RCTGetFatalHandler(void)
187188

188189
return [NSString stringWithFormat:@"%@%@", message, prettyStack];
189190
}
191+
192+
void RCTFatalException(NSException *exception)
193+
{
194+
_RCTLogNativeInternal(RCTLogLevelFatal, NULL, 0, @"%@: %@", exception.name, exception.reason);
195+
196+
RCTFatalExceptionHandler fatalExceptionHandler = RCTGetFatalExceptionHandler();
197+
if (fatalExceptionHandler) {
198+
fatalExceptionHandler(exception);
199+
} else {
200+
#if DEBUG
201+
@try {
202+
#endif
203+
@throw exception;
204+
#if DEBUG
205+
} @catch (NSException *e) {}
206+
#endif
207+
}
208+
}
209+
210+
void RCTSetFatalExceptionHandler(RCTFatalExceptionHandler fatalExceptionHandler)
211+
{
212+
RCTCurrentFatalExceptionHandler = fatalExceptionHandler;
213+
}
214+
215+
RCTFatalExceptionHandler RCTGetFatalExceptionHandler(void)
216+
{
217+
return RCTCurrentFatalExceptionHandler;
218+
}
219+

React/CxxModule/RCTNativeModule.mm

+4
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,14 @@ static MethodCallResult invokeInner(RCTBridge *bridge, RCTModuleData *moduleData
112112
@throw exception;
113113
}
114114

115+
#if RCT_DEBUG
115116
NSString *message = [NSString stringWithFormat:
116117
@"Exception '%@' was thrown while invoking %s on target %@ with params %@\ncallstack: %@",
117118
exception, method.JSMethodName, moduleData.name, objcParams, exception.callStackSymbols];
118119
RCTFatal(RCTErrorWithMessage(message));
120+
#else
121+
RCTFatalException(exception);
122+
#endif
119123
}
120124

121125
return folly::none;

0 commit comments

Comments
 (0)