react-native-macos/React/Base/RCTAssert.m

225 строки
7.6 KiB
Mathematica
Исходник Обычный вид История

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTAssert.h"
#import "RCTLog.h"
2015-04-12 01:08:00 +03:00
NSString *const RCTErrorDomain = @"RCTErrorDomain";
NSString *const RCTJSStackTraceKey = @"RCTJSStackTraceKey";
NSString *const RCTJSRawStackTraceKey = @"RCTJSRawStackTraceKey";
NSString *const RCTFatalExceptionName = @"RCTFatalException";
Extend reason message for `RCTFatalException` (#22532) Summary: Fixes #22530 As described in the issue, the previous behavior for the `RCTFatal` macro was to truncate the `reason` on the resulting `NSException` to 75 characters. This would ensure the reason would fit on a single line, but resulted in issues debugging errors that occurred in the wild, as many crash logging tools (like Sentry) discard the `name` value of the exception and use the `reason` as their primary identifier. At 75 characters, useful information like the location of the error would usually be truncated. - [x] This extends the truncation threshold to 175 characters, which should be short enough to prevent full-screen-takeover length errors, but long enough to provide useful context to the error. - [x] This adds a `userInfo` value to the resulting `NSException`. It copies over the `userInfo` from the `NSError` passed to the macro, and adds an "untruncated message" value that contains the untruncated version of the `NSException`'s reason. [iOS] [Changed] - RCTFatalExceptions now include more information in their reason and a userInfo. <!-- CATEGORY may be: - [General] - [iOS] - [Android] TYPE may be: - [Added] for new features. - [Changed] for changes in existing functionality. - [Deprecated] for soon-to-be removed features. - [Removed] for now removed features. - [Fixed] for any bug fixes. - [Security] in case of vulnerabilities. For more detail, see https://keepachangelog.com/en/1.0.0/#how MESSAGE may answer "what and why" on a feature level. Use this to briefly tell React Native users about notable changes. EXAMPLES: [General] [Added] - Add snapToOffsets prop to ScrollView component [General] [Fixed] - Fix various issues in snapToInterval on ScrollView component [iOS] [Fixed] - Fix crash in RCTImagePicker --> Pull Request resolved: https://github.com/facebook/react-native/pull/22532 Differential Revision: D13373469 Pulled By: cpojer fbshipit-source-id: ac140d14ce76e1664869437c2c178bdd65ab6e0e
2018-12-07 07:19:06 +03:00
NSString *const RCTUntruncatedMessageKey = @"RCTUntruncatedMessageKey";
2015-08-07 16:06:17 +03:00
static NSString *const RCTAssertFunctionStack = @"RCTAssertFunctionStack";
2015-04-12 01:08:00 +03:00
RCTAssertFunction RCTCurrentAssertFunction = nil;
RCTFatalHandler RCTCurrentFatalHandler = nil;
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: https://github.com/facebook/react-native/pull/23691 Reviewed By: fkgozali Differential Revision: D14276366 Pulled By: cpojer fbshipit-source-id: b308d5608e1432d7676447347ae77c0721094e62
2019-03-13 05:24:07 +03:00
RCTFatalExceptionHandler RCTCurrentFatalExceptionHandler = nil;
2015-04-12 01:08:00 +03:00
NSException *_RCTNotImplementedException(SEL, Class);
NSException *_RCTNotImplementedException(SEL cmd, Class cls)
{
NSString *msg = [NSString stringWithFormat:@"%s is not implemented "
"for the class %@", sel_getName(cmd), cls];
return [NSException exceptionWithName:@"RCTNotDesignatedInitializerException"
reason:msg userInfo:nil];
}
2015-04-12 01:08:00 +03:00
void RCTSetAssertFunction(RCTAssertFunction assertFunction)
{
RCTCurrentAssertFunction = assertFunction;
}
RCTAssertFunction RCTGetAssertFunction(void)
{
return RCTCurrentAssertFunction;
}
void RCTAddAssertFunction(RCTAssertFunction assertFunction)
{
RCTAssertFunction existing = RCTCurrentAssertFunction;
if (existing) {
2015-08-07 16:06:17 +03:00
RCTCurrentAssertFunction = ^(NSString *condition,
2015-04-12 01:08:00 +03:00
NSString *fileName,
NSNumber *lineNumber,
NSString *function,
NSString *message) {
existing(condition, fileName, lineNumber, function, message);
assertFunction(condition, fileName, lineNumber, function, message);
};
} else {
RCTCurrentAssertFunction = assertFunction;
}
}
2015-08-07 16:06:17 +03:00
/**
* returns the topmost stacked assert function for the current thread, which
* may not be the same as the current value of RCTCurrentAssertFunction.
*/
static RCTAssertFunction RCTGetLocalAssertFunction()
{
NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary;
NSArray<RCTAssertFunction> *functionStack = threadDictionary[RCTAssertFunctionStack];
RCTAssertFunction assertFunction = functionStack.lastObject;
2015-08-07 16:06:17 +03:00
if (assertFunction) {
return assertFunction;
}
return RCTCurrentAssertFunction;
}
void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction assertFunction)
{
NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary;
NSMutableArray<RCTAssertFunction> *functionStack = threadDictionary[RCTAssertFunctionStack];
2015-08-07 16:06:17 +03:00
if (!functionStack) {
functionStack = [NSMutableArray new];
2015-08-07 16:06:17 +03:00
threadDictionary[RCTAssertFunctionStack] = functionStack;
}
[functionStack addObject:assertFunction];
block();
[functionStack removeLastObject];
}
NSString *RCTCurrentThreadName(void)
{
NSThread *thread = [NSThread currentThread];
NSString *threadName = RCTIsMainQueue() || thread.isMainThread ? @"main" : thread.name;
if (threadName.length == 0) {
const char *label = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
if (label && strlen(label) > 0) {
threadName = @(label);
} else {
threadName = [NSString stringWithFormat:@"%p", thread];
}
}
return threadName;
}
2015-08-07 16:06:17 +03:00
void _RCTAssertFormat(
const char *condition,
const char *fileName,
int lineNumber,
const char *function,
NSString *format, ...)
{
RCTAssertFunction assertFunction = RCTGetLocalAssertFunction();
if (assertFunction) {
va_list args;
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
assertFunction(@(condition), @(fileName), @(lineNumber), @(function), message);
}
}
void RCTFatal(NSError *error)
{
_RCTLogNativeInternal(RCTLogLevelFatal, NULL, 0, @"%@", error.localizedDescription);
RCTFatalHandler fatalHandler = RCTGetFatalHandler();
if (fatalHandler) {
fatalHandler(error);
} else {
#if DEBUG
@try {
#endif
NSString *name = [NSString stringWithFormat:@"%@: %@", RCTFatalExceptionName, error.localizedDescription];
Extend reason message for `RCTFatalException` (#22532) Summary: Fixes #22530 As described in the issue, the previous behavior for the `RCTFatal` macro was to truncate the `reason` on the resulting `NSException` to 75 characters. This would ensure the reason would fit on a single line, but resulted in issues debugging errors that occurred in the wild, as many crash logging tools (like Sentry) discard the `name` value of the exception and use the `reason` as their primary identifier. At 75 characters, useful information like the location of the error would usually be truncated. - [x] This extends the truncation threshold to 175 characters, which should be short enough to prevent full-screen-takeover length errors, but long enough to provide useful context to the error. - [x] This adds a `userInfo` value to the resulting `NSException`. It copies over the `userInfo` from the `NSError` passed to the macro, and adds an "untruncated message" value that contains the untruncated version of the `NSException`'s reason. [iOS] [Changed] - RCTFatalExceptions now include more information in their reason and a userInfo. <!-- CATEGORY may be: - [General] - [iOS] - [Android] TYPE may be: - [Added] for new features. - [Changed] for changes in existing functionality. - [Deprecated] for soon-to-be removed features. - [Removed] for now removed features. - [Fixed] for any bug fixes. - [Security] in case of vulnerabilities. For more detail, see https://keepachangelog.com/en/1.0.0/#how MESSAGE may answer "what and why" on a feature level. Use this to briefly tell React Native users about notable changes. EXAMPLES: [General] [Added] - Add snapToOffsets prop to ScrollView component [General] [Fixed] - Fix various issues in snapToInterval on ScrollView component [iOS] [Fixed] - Fix crash in RCTImagePicker --> Pull Request resolved: https://github.com/facebook/react-native/pull/22532 Differential Revision: D13373469 Pulled By: cpojer fbshipit-source-id: ac140d14ce76e1664869437c2c178bdd65ab6e0e
2018-12-07 07:19:06 +03:00
// Truncate the localized description to 175 characters to avoid wild screen overflows
NSString *message = RCTFormatError(error.localizedDescription, error.userInfo[RCTJSStackTraceKey], 175);
// Attach an untruncated copy of the description to the userInfo, in case it is needed
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
[userInfo setObject:RCTFormatError(error.localizedDescription, error.userInfo[RCTJSStackTraceKey], -1)
forKey:RCTUntruncatedMessageKey];
// Expected resulting exception information:
// name: RCTFatalException: <underlying error description>
// reason: <underlying error description plus JS stack trace, truncated to 175 characters>
// userInfo: <underlying error userinfo, plus untruncated description plus JS stack trace>
@throw [[NSException alloc] initWithName:name reason:message userInfo:userInfo];
#if DEBUG
} @catch (NSException *e) {}
#endif
}
}
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: https://github.com/facebook/react-native/pull/23691 Reviewed By: fkgozali Differential Revision: D14276366 Pulled By: cpojer fbshipit-source-id: b308d5608e1432d7676447347ae77c0721094e62
2019-03-13 05:24:07 +03:00
void RCTSetFatalHandler(RCTFatalHandler fatalHandler)
{
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: https://github.com/facebook/react-native/pull/23691 Reviewed By: fkgozali Differential Revision: D14276366 Pulled By: cpojer fbshipit-source-id: b308d5608e1432d7676447347ae77c0721094e62
2019-03-13 05:24:07 +03:00
RCTCurrentFatalHandler = fatalHandler;
}
RCTFatalHandler RCTGetFatalHandler(void)
{
return RCTCurrentFatalHandler;
}
NSString *RCTFormatError(NSString *message, NSArray<NSDictionary<NSString *, id> *> *stackTrace, NSUInteger maxMessageLength)
{
if (maxMessageLength > 0 && message.length > maxMessageLength) {
message = [[message substringToIndex:maxMessageLength] stringByAppendingString:@"..."];
}
NSString *prettyStack = RCTFormatStackTrace(stackTrace);
return [NSString stringWithFormat:@"%@%@%@", message, prettyStack ? @", stack:\n" : @"", prettyStack ? prettyStack : @""];
}
NSString *RCTFormatStackTrace(NSArray<NSDictionary<NSString *, id> *> *stackTrace) {
if (stackTrace) {
NSMutableString *prettyStack = [NSMutableString string];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\b((?:seg-\\d+(?:_\\d+)?|\\d+)\\.js)"
options:NSRegularExpressionCaseInsensitive
error:NULL];
for (NSDictionary<NSString *, id> *frame in stackTrace) {
NSString *fileName = [frame[@"file"] lastPathComponent];
NSTextCheckingResult *match = fileName != nil ? [regex firstMatchInString:fileName options:0 range:NSMakeRange(0, fileName.length)] : nil;
if (match) {
fileName = [NSString stringWithFormat:@"%@:", [fileName substringWithRange:match.range]];
} else {
fileName = @"";
}
[prettyStack appendFormat:@"%@@%@%@:%@\n", frame[@"methodName"], fileName, frame[@"lineNumber"], frame[@"column"]];
}
return prettyStack;
}
return nil;
}
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: https://github.com/facebook/react-native/pull/23691 Reviewed By: fkgozali Differential Revision: D14276366 Pulled By: cpojer fbshipit-source-id: b308d5608e1432d7676447347ae77c0721094e62
2019-03-13 05:24:07 +03:00
void RCTFatalException(NSException *exception)
{
_RCTLogNativeInternal(RCTLogLevelFatal, NULL, 0, @"%@: %@", exception.name, exception.reason);
RCTFatalExceptionHandler fatalExceptionHandler = RCTGetFatalExceptionHandler();
if (fatalExceptionHandler) {
fatalExceptionHandler(exception);
} else {
#if DEBUG
@try {
#endif
@throw exception;
#if DEBUG
} @catch (NSException *e) {}
#endif
}
}
void RCTSetFatalExceptionHandler(RCTFatalExceptionHandler fatalExceptionHandler)
{
RCTCurrentFatalExceptionHandler = fatalExceptionHandler;
}
RCTFatalExceptionHandler RCTGetFatalExceptionHandler(void)
{
return RCTCurrentFatalExceptionHandler;
}