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

[PLAT-5000] React Native promise rejection stack trace processing #812

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
90 changes: 89 additions & 1 deletion Bugsnag/Payload/BugsnagStackframe.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
//

#import "BugsnagStackframe.h"
#import "BugsnagKeys.h"

#import "BSG_KSBacktrace.h"
#import "BSG_KSDynamicLinker.h"
#import "BugsnagCollections.h"
#import "BugsnagKeys.h"
#import "BugsnagLogger.h"

@implementation BugsnagStackframe

Expand Down Expand Up @@ -66,6 +70,90 @@ + (BugsnagStackframe *)frameFromDict:(NSDictionary *)dict
}
}

+ (NSArray<BugsnagStackframe *> *)stackframesWithCallStackSymbols:(NSArray<NSString *> *)callStackSymbols {
NSString *pattern = (@"^(\\d+)" // Capture the leading frame number
@" +" // Skip whitespace
@"(\\S+)" // Image name
Copy link

@JoeLago JoeLago Mar 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, sorry if it's inappropriate to dig up an old pull request like this. I was trying to use this function to rebuild a stacktrace with some callStackSymbols I cached for a handled error and I believe I'm losing my apps frames because there is a space in our apps display name. For Example:

"0   ReactNative Test                     0x000000010fda7f1b RCTJSErrorFromCodeMessageAndNSError + 79"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reporting this - we are working on a fix: #1036

@" +" // Skip whitespace
@"(0x[0-9a-fA-F]+)" // Capture the frame address
@"(" // Start optional group
@" " // Skip whitespace
@"(.+)" // Capture symbol name
@" \\+ " // Skip " + "
@"\\d+" // Instruction offset
@")?$" // End optional group
);

NSError *error;
NSRegularExpression *regex =
[NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error];
if (!regex) {
bsg_log_err(@"%@", error);
return nil;
}

NSMutableArray<BugsnagStackframe *> *frames = [NSMutableArray array];

for (NSString *string in callStackSymbols) {
NSTextCheckingResult *match = [regex firstMatchInString:string options:0 range:NSMakeRange(0, string.length)];
if (match.numberOfRanges != 6) {
continue;
}
NSString *frameNumber = [string substringWithRange:[match rangeAtIndex:1]];
NSString *imageName = [string substringWithRange:[match rangeAtIndex:2]];
NSString *frameAddress = [string substringWithRange:[match rangeAtIndex:3]];
NSRange symbolNameRange = [match rangeAtIndex:5];
NSString *symbolName = nil;
if (symbolNameRange.location != NSNotFound) {
symbolName = [string substringWithRange:symbolNameRange];
}
kattrali marked this conversation as resolved.
Show resolved Hide resolved

unsigned long long address = 0;
[[NSScanner scannerWithString:frameAddress] scanHexLongLong:&address];

BugsnagStackframe *frame = [BugsnagStackframe new];
frame.machoFile = imageName;
frame.method = symbolName;
frame.frameAddress = [NSNumber numberWithUnsignedLongLong:address];
frame.isPc = [frameNumber isEqualToString:@"0"];

Dl_info dl_info;
uintptr_t address2 = address;
bsg_ksbt_symbolicate(&address2, &dl_info, 1, 0);
if (dl_info.dli_fname != NULL) {
frame.machoFile = [NSString stringWithUTF8String:dl_info.dli_fname].lastPathComponent;
}
if (dl_info.dli_fbase != NULL) {
frame.machoLoadAddress = [NSNumber numberWithUnsignedLongLong:(uintptr_t)dl_info.dli_fbase];
}
if (dl_info.dli_saddr != NULL) {
frame.symbolAddress = [NSNumber numberWithUnsignedLongLong:(uintptr_t)dl_info.dli_saddr];
}
if (dl_info.dli_sname != NULL) {
frame.method = [NSString stringWithUTF8String:dl_info.dli_sname];
}

BSG_Mach_Header_Info *header = bsg_mach_headers_image_at_address(address);
if (header != NULL) {
frame.machoVmAddress = [NSNumber numberWithUnsignedLongLong:header->imageVmAddr];
if (header->uuid != nil) {
CFUUIDRef uuidRef = CFUUIDCreateFromUUIDBytes(NULL, *(CFUUIDBytes *)header->uuid);
frame.machoUuid = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuidRef);
CFRelease(uuidRef);
}
}

[frames addObject:frame];
}

return [NSArray arrayWithArray:frames];
}

- (NSString *)description {
return [NSString stringWithFormat:@"<BugsnagStackframe: %p { %@ %p %@ }>", self,
self.machoFile.lastPathComponent, (void *)self.frameAddress.unsignedLongLongValue, self.method];
}

- (NSDictionary *)toDictionary {
NSMutableDictionary *dict = [NSMutableDictionary new];
BSGDictInsertIfNotNil(dict, self.machoFile, BSGKeyMachoFile);
Expand Down
7 changes: 7 additions & 0 deletions Bugsnag/include/Bugsnag/BugsnagStackframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,11 @@
*/
@property BOOL isLr;

/**
* Returns an array of stackframe objects representing the provided call stack strings.
*
* The call stack strings should follow the format used by `[NSThread callStackSymbols]` and `backtrace_symbols()`.
*/
+ (NSArray<BugsnagStackframe *> *_Nullable)stackframesWithCallStackSymbols:(NSArray<NSString *> *_Nonnull)callStackSymbols;

@end
63 changes: 63 additions & 0 deletions Tests/BugsnagStackframeTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//

#import <XCTest/XCTest.h>

#import "BSG_KSMachHeaders.h"
#import "BugsnagStackframe.h"

@interface BugsnagStackframe ()
Expand Down Expand Up @@ -93,4 +95,65 @@ - (void)testInvalidFrame {
XCTAssertNil(frame);
}

- (void)testDummyCallStackSymbols {
bsg_mach_headers_initialize(); // Prevent symbolication

NSArray<BugsnagStackframe *> *stackframes = [BugsnagStackframe stackframesWithCallStackSymbols:@[]];
XCTAssertEqual(stackframes.count, 0);

stackframes = [BugsnagStackframe stackframesWithCallStackSymbols:@[
@"",
@"1",
@"ReactNativeTest",
@"0x0000000000000000",
@"__invoking___ + 140"]];
XCTAssertEqual(stackframes.count, 0, @"Invalid stack frame strings should be ignored");

stackframes = [BugsnagStackframe stackframesWithCallStackSymbols:@[
@"0 ReactNativeTest 0x000000010fda7f1b RCTJSErrorFromCodeMessageAndNSError + 79",
@"1 ReactNativeTest 0x000000010fd76897 __41-[RCTModuleMethod processMethodSignature]_block_invoke_2.103 + 97",
@"2 ReactNativeTest 0x000000010fccd9c3 -[BenCrash asyncReject:rejecter:] + 106",
@"3 CoreFoundation 0x00007fff23e44dec __invoking___ + 140",
@"4 CoreFoundation 0x00007fff23e41fd1 -[NSInvocation invoke] + 321",
@"5 CoreFoundation 0x00007fff23e422a4 -[NSInvocation invokeWithTarget:] + 68",
@"6 ReactNativeTest 0x000000010fd76eae -[RCTModuleMethod invokeWithBridge:module:arguments:] + 578",
@"7 ReactNativeTest 0x000000010fd79138 _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dynamicE + 246"]];
XCTAssertEqual(stackframes.count, 8);
kattrali marked this conversation as resolved.
Show resolved Hide resolved

stackframes = [BugsnagStackframe stackframesWithCallStackSymbols:@[
@"0 ReactNativeTest 0x000000010fda7f1b",
@"1 ReactNativeTest 0x000000010fd76897",
@"2 ReactNativeTest 0x000000010fccd9c3",
@"3 CoreFoundation 0x00007fff23e44dec",
@"4 CoreFoundation 0x00007fff23e41fd1",
@"5 CoreFoundation 0x00007fff23e422a4",
@"6 ReactNativeTest 0x000000010fd76eae",
@"7 ReactNativeTest 0x000000010fd79138"]];
XCTAssertEqual(stackframes.count, 8, @"Symbol name and offset are optional; stack frames should still be parsed if they are omitted");
}

- (void)testRealCallStackSymbols {
bsg_mach_headers_register_for_changes(); // Ensure call stack can be symbolicated

NSArray<NSString *> *callStackSymbols = [NSThread callStackSymbols];
NSArray<BugsnagStackframe *> *stackframes = [BugsnagStackframe stackframesWithCallStackSymbols:callStackSymbols];
XCTAssertEqual(stackframes.count, callStackSymbols.count, @"All valid stack frame strings should be parsed");
XCTAssertTrue(stackframes.firstObject.isPc, @"The first stack frame should have isPc set to true");
[stackframes enumerateObjectsUsingBlock:^(BugsnagStackframe *stackframe, NSUInteger idx, BOOL *stop) {
XCTAssertNotNil(stackframe.frameAddress);
XCTAssertNotNil(stackframe.machoFile);
XCTAssertNotNil(stackframe.method);
if (idx == stackframes.count - 1 && stackframe.machoLoadAddress == nil) {
// The last callStackSymbol is often not in any Mach-O image, e.g.
// "41 ??? 0x0000000000000005 0x0 + 5"
return;
}
XCTAssertNotNil(stackframe.machoUuid);
XCTAssertNotNil(stackframe.machoVmAddress);
XCTAssertNotNil(stackframe.machoLoadAddress);
XCTAssertNotNil(stackframe.symbolAddress);
XCTAssertTrue([callStackSymbols[idx] containsString:stackframe.method]);
}];
}

@end